Migrating from Legacy Counterparties

This page describes how to migrate your integration from the legacy counterparty schema to the typed counterparty schema for transaction events.

Alloy still supports the legacy schema, and there's no deprecation date for it. However, we recommend that you migrate if your external_counterparty_id represents a person on some rails and a bank account on others, or if you want to build rules that track a single counterparty across multiple accounts and cards.

Typed counterparties start with no history, and Alloy doesn't convert the legacy counterparties that are already on file. To add typed counterparties to older transactions, see Optional: Backfill older transactions.


Legacy counterparty schema

In the legacy schema, a transaction event carries one flat counterparty object that represents the other side of the transaction: the party that isn't your institution's account. Either account_number or external_counterparty_id is required. Every other field is optional and sits at the same level of the object.

The following table groups the legacy fields:

GroupLegacy fields
Keyexternal_counterparty_id, account_number
Name and typename_first, name_last, account_name, counterparty_type
Addressaddress_line_1, address_line_2, address_city, address_state, address_postal_code, address_country_code
Bank accountrouting_number, account_number
Card and merchantcard_network, name_on_card, avs, merchant_category_code, merchant_category_description
Otherdescription, currency

Because the legacy schema has only one key, you can't tell Alloy what that key represents. Most integrations use external_counterparty_id in one of the following ways:

  • As an identity: the ID represents a person, business, or merchant. For example, a payroll provider that sends direct deposits from five different bank accounts is one counterparty.
  • As an instrument: the ID represents a bank account, a card, or another payment instrument. With this mapping, the same payroll provider is five counterparties, one for each funding account.

Both approaches are valid, and many integrations mix them depending on the payment rail. For example, an integration might key ACH transactions on the external account and card transactions on the merchant. As a result, rules that count or aggregate counterparties can compare two different things. A rule for distinct counterparties in 30 days counts people on one rail and accounts on another.

The following example shows a legacy ACH withdrawal to the accountholder's linked external account:

{
    "event_type": "transaction",
    "event_lifecycle_id": "5071950e-adec-4f11-bd83-6e77af67d461",
    "event_status": "Completed",
    "data": {
        "external_transaction_id": "5071950e-adec-4f11-bd83-6e77af67d461",
        "external_account_id": "f3f49249-dc28-4f90-a5ae-c7978306d03b",
        "external_entity_id": "7dabe929-c4a3-44bf-86cd-75e9bb049a79",
        "timestamp": "2026-10-03T16:40:00Z",
        "transaction_created_date": "2026-10-03T16:40:00Z",
        "amount": -180000,
        "currency": "USD",
        "process_method": "ACH",
        "category": "Withdrawal",
        "is_triggered_by_user": true,
        "status": "Completed",
        "counterparty": {
            "external_counterparty_id": "14aa4e71-9d3c-4dec-80a6-1f933d6c51e3",
            "account_name": "Johnny Appleseed",
            "routing_number": "981234568",
            "account_number": "004455667788"
        }
    }
}

Nothing in this payload tells Alloy whether 14aa4e71-9d3c-4dec-80a6-1f933d6c51e3 represents the bank account or the person who owns it. For the typed version of this transaction, see ACH withdrawal to a linked external account.


Field mapping

In the legacy counterparty schema, the same flat fields often mean different things on different payment rails. For example, external_counterparty_id might represent the payer company on an ACH transaction, the recipient's bank account on a wire, and either the card or the shopper on a card payment. Similarly, account_name might be a business name, the name on an account, or a generic product name.

The typed schema removes most of that ambiguity, because identity fields always describe who the counterparty is and instrument fields always describe how they paid. What still varies by rail is which objects you send. An inbound wire might include only an instrument, an in-store card purchase might include only an identity, and an online card payment typically includes both.

The following table maps each legacy field to its typed field:

Legacy fieldTyped fieldDescription
external_counterparty_ididentity.external_counterparty_identity_id or instrument.external_instrument_idIf the ID represents a person, business, or merchant, use identity. If it represents an account, card, or wallet, use instrument. Reuse the same value so that your own records still match.
account_number used as the keyinstrument.external_instrument_idAccount number alone no longer identifies a counterparty. Build a stable instrument ID instead, such as a token or hash of the routing and account number.
account_numberinstrument.account_number or instrument.account_number_last4
routing_numberinstrument.routing_numberIf you know the routing number type, also send routing_number_type.
name_first, name_lastidentity.name_first, identity.name_last
account_nameidentity.identity_name or instrument.account_holder_nameIf the field contains the business or merchant name, use identity. If it contains the name on the account, use instrument.
counterparty_typeidentity.typeMap your existing values to person, business, or merchant.
address_*identity.address_*The field names don't change.
card_networkinstrument.card_network
name_on_cardinstrument.name_on_card
avsinstrument.avs
merchant_category_codeidentity.merchant_category_codeThe merchant category code (MCC) describes the merchant, not the card.
merchant_category_descriptionidentity.mcc_descriptionThe field name changes.
currencyinstrument.currency
descriptionNoneMove this value to the transaction's top-level description field. Alloy rejects an event that sends a flat field alongside identity or instrument.

The following fields are new and have no legacy equivalent: identity.birth_date, email_address, phone_number, merchant_identifier, dba_name, device, external_entity_id, and instrument.account_type, card_bin, card_last4, form_factor, expiration_month, expiration_year, wallet_provider, wallet_identifier, check_number, payee_name_raw, country_code.

For the full list of identity and instrument fields, see the Create an event API reference.


Migrate your integration

To migrate your integration, follow these steps:

  1. List each payment rail and process_method that you send to Alloy, and note what external_counterparty_id represents on each one. You can use the table at the top of Counterparty scenarios as a template.
  2. For each rail, decide where the identity ID and instrument ID come from. Good sources include payee IDs, merchant IDs, processor card fingerprints, and bank account tokens.
  3. Update your counterparty mapping by using the field mapping table. Move any flat fields that you can't map, such as description, out of counterparty.
  4. To confirm that Alloy accepts your updated payloads, send test events in your sandbox environment.
  5. Switch one rail at a time. You can send legacy and typed events side by side during the migration.
  6. With your Alloy team, review any rules and Workflows that use counterparty attributes. Because typed counterparties start with no history, lookback rules see only typed events until you backfill.
  7. Optional: If your rules need history, backfill older transactions. For more information, see the following section.

Optional: Backfill older transactions

To add a typed counterparty to a transaction that Alloy already received, send another transaction event with the same external_transaction_id and the typed counterparty object. Alloy treats the event as an update to the existing transaction.


FAQ

Can I keep sending the legacy schema? Yes. Alloy still supports the legacy schema, and there's no deprecation date for it.

Can I reuse my existing external_counterparty_id values? Yes, and we recommend it. Put each value in the typed field that matches what it represented in your legacy integration.

Can I send the legacy and typed schemas on the same event? No. A single counterparty object must use one schema or the other. Alloy rejects an event that mixes them.


Did this page help you?