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:
| Group | Legacy fields |
|---|---|
| Key | external_counterparty_id, account_number |
| Name and type | name_first, name_last, account_name, counterparty_type |
| Address | address_line_1, address_line_2, address_city, address_state, address_postal_code, address_country_code |
| Bank account | routing_number, account_number |
| Card and merchant | card_network, name_on_card, avs, merchant_category_code, merchant_category_description |
| Other | description, 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 field | Typed field | Description |
|---|---|---|
external_counterparty_id | identity.external_counterparty_identity_id or instrument.external_instrument_id | If 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 key | instrument.external_instrument_id | Account 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_number | instrument.account_number or instrument.account_number_last4 | |
routing_number | instrument.routing_number | If you know the routing number type, also send routing_number_type. |
name_first, name_last | identity.name_first, identity.name_last | |
account_name | identity.identity_name or instrument.account_holder_name | If the field contains the business or merchant name, use identity. If it contains the name on the account, use instrument. |
counterparty_type | identity.type | Map your existing values to person, business, or merchant. |
address_* | identity.address_* | The field names don't change. |
card_network | instrument.card_network | |
name_on_card | instrument.name_on_card | |
avs | instrument.avs | |
merchant_category_code | identity.merchant_category_code | The merchant category code (MCC) describes the merchant, not the card. |
merchant_category_description | identity.mcc_description | The field name changes. |
currency | instrument.currency | |
description | None | Move 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:
- List each payment rail and
process_methodthat you send to Alloy, and note whatexternal_counterparty_idrepresents on each one. You can use the table at the top of Counterparty scenarios as a template. - 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.
- Update your
counterpartymapping by using the field mapping table. Move any flat fields that you can't map, such asdescription, out ofcounterparty. - To confirm that Alloy accepts your updated payloads, send test events in your sandbox environment.
- Switch one rail at a time. You can send legacy and typed events side by side during the migration.
- 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.
- 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.
Updated about 6 hours ago