Before you start: the OMS API is in early access. Every endpoint, including the ones in this guide, requires an early-access API key. Request access before you begin.Authenticate by exchanging your API key and secret for a bearer token at
POST /auth/token, then send it as Authorization: Bearer {accessToken} on every request. Every POST that creates or changes a resource also requires an Idempotency-Key header. See Get started for the full flow.Prerequisites
- A customer with a
cst_ID whosecryptoCustodyandusdendorsements areactive: the payout is funded from this customer’s wallet. - A funded OMS wallet with a
wlt_ID holding enough USDC to cover the payout. - The recipient’s details: their legal name, and their bank account information.
Step 1: Create the counterparty
POST /counterparties requires the owning customerId, an entityType discriminator, and the name fields for that type:
entityType: "individual"requiresfirstNameandlastName, with optionaldateOfBirth.entityType: "business"requiresbusinessName.
address is also required unless your project is configured not to require one. Other identity fields are optional but improve compliance screening: email, phone, taxId, and nationality.
The request is strictly validated: a missing or unrecognized entityType returns 422 EntityTypeInvalid, and a flat name field is rejected. Each entityType arm rejects keys from the other arm, so firstName on a business request (or businessName on an individual request) returns 422 Validation.
Request
address, OMS validates it in full: non-blank line1, city, and zipCode, a two-letter ISO country, and state for a US address. line2 is optional. A counterparty created without an address omits address from its responses.
Response, 201 Created
Responses echo the split name fields and add a read-only name, server-composed as businessName for a business or firstName + " " + lastName for an individual. The composed value is at most 140 characters.
individual counterparty, the response carries firstName, lastName, and optional dateOfBirth in place of businessName, and name renders as "{firstName} {lastName}".
Store the ctp_ ID. A counterparty that fails compliance screening comes back rejected with a failure object whose code says why; only active counterparties can own external accounts. Screening also runs on PATCH /counterparties/{counterpartyId}: an update can move a counterparty to rejected, and a counterparty rejected only for countryProhibited returns to active when an update moves its address to an allowed country.
Address changes on an existing counterparty are screened the same way. A PATCH /counterparties/{counterpartyId} that resolves to a prohibited country returns 200 with status: rejected and failure.code: countryProhibited, and fires counterparty.statusChanged. Quote and transaction flows in this guide refuse payouts to external accounts owned by a rejected counterparty until the counterparty is updated back to an allowed country. Supplying an allowed country on a counterparty rejected with countryProhibited reactivates it; a counterparty rejected for any other reason stays rejected.
Step 2: Register the counterparty’s bank account
Register the recipient’s bank withPOST /external-accounts, setting the owner to the counterparty. The per-type detail object must match the type: bankUs for US accounts, bankIban for international IBAN accounts, or bankCanada for Canadian accounts.
Request
Response, 201 Created
details.accountNumberLast4. owner names the counterparty, and customer is the customer the account is registered for. The account starts pending and moves to active (usable on quotes), or to failed or rejected with a failure. Wait for active, or externalAccount.statusChanged, before quoting against it.
Beneficiary identity on a US bank payout
When the destination is a counterparty-ownedbankUs account, OMS registers the beneficiary with the paying bank under the counterparty’s own structured identity: the entityType, the matching name fields (firstName and lastName, or businessName), and the counterparty’s address. The customer’s identity is not used for a counterparty-owned account. Register US bank counterparties with the complete entityType, name, and address fields shown in Step 1; a counterparty without entityType cannot be used as a bankUs destination and is rejected with 422 AccountTypeRequirementsNotMet. A payout the receiving bank returns as a payee-name mismatch flips the external account to invalid with failure.code: payeeNameMismatch.
Step 3: Create the quote
The quote is identical to any other bank payout: thesource is the customer’s OMS wallet, and the destination is the counterparty’s registered bank account referenced by its ext_ ID.
owner block, so you can render who is being paid without extra reads:
owner.id for the recipient’s full record (entity type, address, contact fields). All amounts and fees live in the top-level pricing object, and sourceToDestination is cryptoToFiatAccount. The bank destination carries no rail; the rail is on the payout transfer once it exists.
Step 4: Execute and track
Accept the quote withPOST /transactions and the quoteId:
processing and moves to completed when the bank payout settles, or failed with a failure object. The payout leg surfaces granular progress in subStatus: processing.awaitingFiatOut while the payout is queued, then processing.fiatOut once it is in flight. If the payout fails and is retried, subStatus reads processing.payoutRetrying; if the provider holds it until your prefunded balance covers it, processing.payoutHeld. Poll GET /transactions/{transactionId} or subscribe to transaction.statusChanged; see the webhook events catalog.
To reconcile a payout run, filter GET /transactions?customerId=...: each transaction’s destination owner.id is the counterparty it paid (owner.kind: "counterparty").
To see the name the recipient’s bank shows as the sender, read payoutOrigin.accountHolderName on the quote or transaction. payoutOrigin.accountHolder says whose identity that is: customer, partner, or paymentProvider.
Managing counterparties
GET /counterparties?customerId=...lists a customer’s counterparties. Paginate withlimit,startingAfter, andendingBefore.GET /counterparties/{counterpartyId}fetches one by ID.PATCH /counterparties/{counterpartyId}partially updates identity fields; omitted fields are unchanged.entityTypeis immutable, and bothentityTypeand the composednamefield are rejected with400. Name updates go through the split fields for the storedentityType(firstNameandlastNameforindividual;businessNameforbusiness); the server recomposes and echoes backname. A name field that does not belong to the storedentityTypereturns422 Validation, and an empty body returns422 EmptyUpdate.DELETE /counterparties/{counterpartyId}soft-deletes. It returns409 CounterpartyHasActiveExternalAccountswhile the counterparty still owns active external accounts: delete those accounts first, then delete the counterparty.
Related
- B2B payouts: running payouts to many recipients
- Bank transfers: rails, per-type bank fields, and registration details
- External accounts: the full registration and lifecycle model
- Crypto to fiat: the underlying quote and transaction flow