> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polygon.technology/llms.txt
> Use this file to discover all available pages before exploring further.

# External accounts

> Off-platform bank accounts, external wallets, and debit cards, referenced by ID on a quote.

External accounts are off-platform funding and payout endpoints: bank accounts, external wallets, and debit cards. Each is referenced by ID on a quote side. OMS never holds funds in an external account; it is only a source or destination reference on a transaction.

A quote's source is always an OMS wallet or a card, so banks are always destinations. Cards can be either a source (pull-from-card) or a destination (push-to-card). External wallets are crypto destinations.

An external account belongs either to a customer directly or to one of the customer's counterparties (a saved third-party payee). You register accounts with `POST /external-accounts` and reference the returned `ext_` ID on quotes.

## Supported types

Every external account ID carries the same `ext_` prefix, whatever the instrument. The `type` field tells you which instrument type to select on the quote side.

| `type` | Instrument | Role on a quote |
| - | - | - |
| `bankUs` | US bank account (ACH, same-day ACH, wire) | Destination |
| `bankIban` | IBAN bank account (international, SWIFT) | Destination |
| `bankCanada` | Canadian bank account | Destination |
| `walletExternal` | External wallet (held outside OMS) | Destination |
| `card` | Debit card | Source or destination |

## Referencing an external account in a transaction

Reference the external account ID in the quote's `destination` (or `source`, for cards). The instrument is typed by `type`, and the `id` in `details` carries the `ext_` prefix.

### US bank destination

```json theme={null}
{
  "customerId": "cst_...",
  "source": {
    "type": "walletCrypto",
    "details": { "id": "wlt_...", "asset": "usdc", "network": "polygon" },
    "amount": "500.00"
  },
  "destination": {
    "type": "bankUs",
    "details": {
      "id": "ext_...",
      "asset": "usd",
      "network": "ach",
      "accountHolder": "customer"
    }
  }
}
```

The US bank `network` is one of `ach`, `achSameDay`, or `wire`. The `accountHolder` field is always `customer`. IBAN (`bankIban`) and Canadian (`bankCanada`) destinations follow the same pattern with their respective `ext_` IDs.

### External wallet destination

```json theme={null}
{
  "destination": {
    "type": "walletExternal",
    "details": { "id": "ext_...", "asset": "usdc", "network": "polygon" }
  }
}
```

For an external wallet on a quote you supply exactly one of `id` (a registered external account, `ext_` prefix) or `blockchainAddress` (a raw on-chain address). Which form a route accepts depends on its corridor: a crypto source takes `id`, a `card` source takes either, and a route that cannot deliver to an external wallet rejects `id` with `WalletExternalIDUnsupported`.

### Card source or destination

```json theme={null}
{
  "source": {
    "type": "card",
    "details": { "id": "ext_...", "asset": "usd" },
    "amount": "100.00"
  }
}
```

A card requires `asset: usd`; its network is inferred server-side. Reference it as the `source` to pull from a card or as the `destination` to push to one. See [Debit cards](/payments/debit-cards) for both directions.

## Registering external accounts

Register external accounts with `POST /external-accounts`. The body carries an `owner`, a `type`, and exactly one per-type detail object whose key matches `type`; a mismatch is rejected with `422`. Optional `label` and `metadata` fields round out the request. The create endpoint accepts `type: bankUs`, `bankIban`, `bankCanada`, or `walletExternal`. Cards register through their own endpoint and are not valid on `POST /external-accounts`; see [Registering a card](/payments/debit-cards#registering-a-card).

The `owner` selects who the account belongs to:

* `{ "kind": "customer", "customerId": "cst_..." }` for an account owned by the customer.
* `{ "kind": "counterparty", "counterpartyId": "ctp_..." }` for an address-book entry owned by one of the customer's counterparties.

<CodeGroup>
  ```json US bank theme={null}
  {
    "owner": { "kind": "customer", "customerId": "cst_..." },
    "type": "bankUs",
    "bankUs": {
      "accountNumber": "123456789012",
      "routingNumber": "021000021",
      "accountType": "checking",
      "bankName": "Chase"
    },
    "label": "Primary checking"
  }
  ```

  ```json IBAN bank theme={null}
  {
    "owner": { "kind": "counterparty", "counterpartyId": "ctp_..." },
    "type": "bankIban",
    "bankIban": {
      "iban": "DE89370400440532013000",
      "BIC": "COBADEFFXXX"
    }
  }
  ```

  ```json Canadian bank theme={null}
  {
    "owner": { "kind": "customer", "customerId": "cst_..." },
    "type": "bankCanada",
    "bankCanada": {
      "institutionNumber": "003",
      "transitNumber": "12345",
      "accountNumber": "1234567"
    }
  }
  ```

  ```json External wallet theme={null}
  {
    "owner": { "kind": "customer", "customerId": "cst_..." },
    "type": "walletExternal",
    "walletExternal": {
      "blockchainAddress": "0xabc123...",
      "networkFamily": "evm",
      "custodian": "COINBASE_US"
    }
  }
  ```
</CodeGroup>

Per-type required and optional fields:

| Type | Required | Optional |
| - | - | - |
| `bankUs` | `accountNumber`, `routingNumber` (nine-digit ABA) | `accountType` (`checking` or `savings`, defaults to `checking` when omitted), `bankName` |
| `bankIban` | `iban`, `BIC` (8 or 11 characters) | `countryCode` (derived from the IBAN's first two letters when omitted), `bankAddress` |
| `bankCanada` | `institutionNumber` (three digits), `transitNumber` (five digits), `accountNumber` | `bankName` |
| `walletExternal` | `blockchainAddress`, `networkFamily` (`evm`, `solana`, or `tron`), `custodian` (unless your project does not require it) | `otherCustodian` (required when `custodian` is `OTHER`) |

When your project requires `custodian` and the request omits it, create returns `422 CustodianRequired`. When your project does not require it, the response omits `custodian` if you did not send one.

An external wallet is registered per network family: an EVM address registered once is valid across every EVM chain. For `networkFamily: tron`, the `blockchainAddress` must be a mainnet Base58Check address (leading `T`, case-sensitive); the zero account is rejected.

Provisioning runs synchronously against the configured payment provider: the account starts `pending` and flips to `active` or `failed`.

### SWIFT-supported destination countries

IBAN and Canadian bank registrations are screened at create time against the list of countries OMS can pay out to over SWIFT. If the destination country (derived from the IBAN, an explicit `countryCode`, or the BIC when neither is available; always Canada for `bankCanada`) is not on that list, the create returns `201` with `status: rejected` and `rejectionReason: countryNotSupported`, and no vendor call is made. `bankUs`, `walletExternal`, and `card` registrations are not screened this way.

### Duplicate submissions

`POST /external-accounts` and `POST /external-accounts/cards` deduplicate by owner. A duplicate registration for the same owner returns `409 externalAccountAlreadyRegistered`; a registration of the same instrument under a different owner of the same customer returns a new `201` with a distinct `ext_` ID.

For wallet duplicates the `409` body carries a `details` object that identifies the existing record:

```json theme={null}
{
  "error": "externalAccountAlreadyRegistered",
  "code": "externalAccountAlreadyRegistered",
  "details": {
    "existingAccountId": "ext_...",
    "existingAccountStatus": "active",
    "matchedOn": "blockchainAddressNetworkFamilyOwner"
  }
}
```

`existingAccountStatus` is the current status of the surviving record. `active` and `pending` both return `409`, so a client cannot use retry to force a re-drive of a `pending` row: recovery from a stuck `pending` runs asynchronously through the stale-pending sweeper. `failed` and `rejected` rows are transparently re-driven under their existing `ext_` ID and adopt the resubmitted `label` and `metadata`.

Card duplicates return the same `409 externalAccountAlreadyRegistered` code with no `details` object, because the card provider's dedupe is global and OMS cannot name a specific account.

Both create routes require the `Idempotency-Key` header and enforce replay: an identical retry with the same key returns the original response.

### Response

Sensitive fields are write-only. Full account numbers, IBANs, card numbers, and CVVs are never stored on reads or echoed back; the response carries only their last-four renderings (`accountNumberLast4`, `ibanLast4`, `cardNumberLast4`). Exactly one per-type response object is populated, selected by `type`. For `bankUs`, `accountType` is always present on reads: `checking` when it was omitted at create time, otherwise the value you supplied.

```json theme={null}
{
  "id": "ext_...",
  "object": "externalAccount",
  "owner": { "kind": "customer", "customerId": "cst_..." },
  "type": "bankUs",
  "category": "fiatAccount",
  "status": "pending",
  "bankUs": {
    "accountNumberLast4": "9012",
    "routingNumber": "021000021",
    "accountType": "checking",
    "bankName": "Chase"
  },
  "label": "Primary checking",
  "createdAt": "2026-07-07T10:30:00Z",
  "updatedAt": "2026-07-07T10:30:00Z"
}
```

### Owner rendering

Customer-owned external accounts carry `ownerDisplayName`, a ready-to-render label for the owning customer, alongside a `customer` summary:

```json theme={null}
"ownerDisplayName": "Jane Doe",
"customer": { "id": "cst_...", "name": "Jane Doe" }
```

Both fields are populated on `GET /external-accounts`, `GET /external-accounts/{externalAccountId}`, and `PATCH /external-accounts/{externalAccountId}` responses when `owner.kind` is `customer`. Counterparty-owned accounts omit both fields; OMS has no name resolver for counterparties on this resource. See [Customer summary on a transaction](/payments/transactions#customer-summary-on-a-transaction) for the shared shape.

### Released sender-attribution holds

When you register a `walletExternal` account whose address matches inbound transfers held for sender attribution, the create response includes `resolvedTransactions`: the IDs of the transactions the registration submitted for release. Attribution is asynchronous; each transaction moves from `awaitingAction.awaitingSenderAttribution` to `processing` once the provider confirms settlement, so an immediate `GET` on one of the IDs may still show `awaitingAction`. The field appears only on the `POST` create response, never on reads.

## Managing external accounts

* `GET /external-accounts` lists external accounts in the project. `customerId`, `counterpartyId`, and `status` are optional filters; omit them all to list every non-deleted account in the project. `status=deleted` explicitly lists soft-deleted rows. Paginate with `limit`, `startingAfter`, and `endingBefore`.
* `GET /external-accounts/{externalAccountId}` fetches a single account by ID.
* `PATCH /external-accounts/{externalAccountId}` updates `label` and `metadata` only. Financial and identity fields are immutable; to change a destination, register a new account and re-point to it. Any other key in the body is rejected with `400`.
* `DELETE /external-accounts/{externalAccountId}` soft-deletes the account and returns `204`.

## Statuses

| Status | Meaning |
| - | - |
| `pending` | Submitted to the provider; provisioning in progress. |
| `active` | Usable on quotes. |
| `rejected` | Refused at create time; `rejectionReason` identifies the cause (for example, a country prohibited by compliance screening, or an international destination whose country is outside OMS's SWIFT coverage, `countryNotSupported`). |
| `invalid` | Became unusable after activation (for example, following an ACH payout return). `invalidReason` identifies the cause; see [Invalid external accounts](#invalid-external-accounts). |
| `failed` | Provisioning failed or the provider rejected the account; `failureReason` identifies the cause. |
| `deleted` | Soft-deleted. |

## Invalid external accounts

When a payout to an active external account is rejected with a confirmed rail-specific return code, OMS transitions the account to `status: invalid` and stamps a closed-enum `invalidReason` on it. The transition fires an `externalAccount.statusChanged` webhook, and every active [virtual account](/payments/virtual-accounts) or [deposit address](/payments/deposit-addresses) that references the invalidated account flips to `inactiveActionRequired` so it can no longer route new payouts to a known-bad destination.

| `invalidReason` | Meaning |
| - | - |
| `accountClosed` | The receiving bank reported the account closed. |
| `accountFrozenByBank` | The receiving bank reported the account frozen. |
| `routingOrAccountNumberInvalid` | The routing or account number was rejected as invalid. |
| `accountHolderDeceased` | The receiving bank reported the account holder deceased. |
| `accountDoesNotSupportTransfers` | The receiving account cannot accept the requested transfer type. |
| `payeeNameMismatch` | The payee name on the payout did not match the account holder on file. |
| `walletUnreachableOnNetwork` | An on-chain destination was reported unreachable on the target network. |
| `billingAddressMismatch` | The card issuer rejected the transaction on billing-address verification. |
| `cardExpired` | The card is past its expiration date. |
| `cardClosedOrLostStolen` | The card issuer reported the card closed, lost, or stolen. |
| `pushToCardUnsupported` | The card does not support push-to-card payouts. |
| `cardDeclinedByIssuer` | The card issuer declined the payout. |

The account also carries an `invalidReasonCode` field with the provider's own code for the underlying failure (for example, a NACHA return code such as `R15` on a US bank ACH payout). Use `invalidReason` to decide what to do; use `invalidReasonCode` when you need the exact provider code for support or reconciliation.

The identifiers on an external account are immutable; there is no update path that can repair an invalid account. Register a new external account with the corrected details and re-point any dependent virtual accounts or deposit addresses to it to recover.

Wire and SWIFT rails do not currently populate `invalidReason`; a wire payout failure surfaces on the transaction itself, not on the destination external account.

## Related

* [Debit cards](/payments/debit-cards): using a card as a funding source or payout destination
* [Bank transfers guide](/api-reference/guide-bank-transfers): ACH and wire transfer walkthrough
* [Transactions](/payments/transactions): how to use an external account on a quote
