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.Flow
Prerequisites
- A bearer token for your project. Whether customers need identity verification (KYC for individuals, KYB for businesses) is set per project, not per request, so check with us which policy your project runs.
- Optionally, a webhook endpoint subscribed to
customer.statusChangedso you learn when onboarding finishes. See Receiving webhooks.
Step 1: Create the customer
SendPOST /customers with the end user’s identity. type is required. An individual also needs firstName and lastName; a business needs companyLegalName and email (see Onboard a business below).
Which fields to send
The spec marks most identity fields optional, but what your project actually requires depends on its verification policy:- If your project verifies individuals and offers custodial wallets, send all of these, or creation fails with
400:emailbirthDateasYYYY-MM-DDphoneas a US number in E.164 form:+1followed by 10 digitsidentifyingInformationwith anssnentry:{ "type": "ssn", "issuingCountry": "US", "number": "123456789" }residentialAddressin the US, withline1,city, a two-letterstate,zipCode, andcountry: "US". Addresses in New York, Texas, and Vermont are not supported on these projects.
- If your project does not verify customers,
typeplus the name fields is enough. Send the identity fields anyway if you plan to enable fiat rails later: a customer without them cannot be provisioned to move USD.
endorsements or metadata on create. Both appear in the spec but are rejected today with cause: "endorsements is not yet supported" (or metadata). OMS requests the default endorsements for you. Fields OMS does not recognize are ignored silently, so check spelling against the API reference if a value you sent never shows up.
Request
externalId so you can find the customer later with GET /customers?externalId=usr_7890. birthDate, residentialAddress, ipAddress, and identifyingInformation are write-only: OMS stores them and never returns them.
Response
OMS answers in one of two ways.201 Created when creation finishes within the request. The customer starts pending while onboarding runs:
202 Accepted when creation outlasts the request. The body carries the ID the customer will have, and a Retry-After header says how many seconds to wait:
GET /customers/{customerId} returns 404. To follow it, repeat the same POST with the same Idempotency-Key and body after Retry-After: the repeat answers 202 while creation is still running and 201 with the customer once it is done. Do not create the customer again with a new key.
If creation fails
Validation errors come back as400 with code 2001 (InvalidArgument). The msg is always "invalid argument"; the reason is in cause:
causenames a field that “is not yet supported”: remove that field and send again. These come back one field per response.causelists required fields (for examplessn is required): your project verifies customers. Add the fields listed in Which fields to send. Acausecan hold several reasons separated by newlines.- Anything you cannot resolve: every response carries a
Request-Idheader. Log it and include it when you contact support.
Step 2: Wait for the customer to become active
Onboarding runs after creation. Watch forcustomer.statusChanged, or poll GET /customers/{customerId}, and branch on status:
Each entry in
endorsements has its own status from the same set, and an endorsement never reads further along than the customer. Custodial wallets need cryptoCustody to be active; bank payouts, virtual accounts, and cash need usd. See Endorsements for what each one gates.
PATCH /customers/{customerId} accepts the same identity fields as create. A change to residentialAddress, lastName, identifyingInformation, or ipAddress re-runs verification, and identifyingInformation entries are appended rather than replaced.
OMS announces a customer it sees for the first time as
pending in customer.created, then sends customer.statusChanged with previousStatus: "pending", even when the customer is already active. Treat the first webhook you receive for a cst_ ID as its creation.Onboard a business
Skip this section for individuals. Create the business withtype: "business", companyLegalName, and email. A business rejects the person-name fields (firstName, middleName, lastName). Send the business facts your project’s KYB policy asks for, such as legalEntityType, registrationCountry, taxIdType, businessTaxId (write-only), and physicalAddress:
1
Add each beneficiary
POST /customers/{customerId}/beneficiaries once per person. roles is required (beneficialOwner, controller, director, signatory, or representative).id and its version. birthDate, taxId, and identificationNumber are write-only; reads report hasBirthDate, hasTaxId, and hasIdentificationNumber. To change a beneficiary, PUT /customers/{customerId}/beneficiaries/{beneficiaryId} with the current version.2
Submit the onboarding
POST /customers/{customerId}/onboardings with one to five beneficiaryIds and a partnerAttestation that records the verification you ran: its id, who ran it (provider), and attestedAt.3
Track the result
The onboarding starts
pending and ends active or rejected; when it does not succeed, failureCode is rejected or processingFailed. The customer stays pending while it runs and moves to rejected if it is rejected. Read it with GET /customers/{customerId}/onboardings/{onboardingId}, or watch customer.statusChanged.Step 3: Provision a custodial wallet
Once the customer’scryptoCustody endorsement is active, create a wallet with POST /wallets. Each custodial wallet holds one asset on one network, and OMS manages the keys.
Request
details.asset is usdc or usdt, and details.network is a supported network such as polygon, base, ethereum, or solana. Call GET /networks for the pairs your project supports. To hold another asset or network, create another wallet.
Response
201 Createdwhen the wallet already exists or is confirmed during the request. The body is the wallet.202 Acceptedwhen the custody provider confirms later. The body is a wallet operation, and theLocationheader points atGET /wallets/operations/{operationId}. Poll it untilstatusiscompleteand readwalletId, or wait forcustomerWallet.statusChanged.
201 body, abridged (the full wallet also carries provider, label, and metadata):
type: "custodial"; responses report type: "walletCrypto". address is left out until the provider reports it. Store the wlt_ ID: it is the source or destination of quotes. Read balances from balances[] on GET /wallets/{walletId}, and list a customer’s wallets with GET /wallets?customerId=....
If wallet creation fails
Step 4: Register a bank account
If the customer will receive fiat payouts, register their bank account withPOST /external-accounts. The body carries an owner, a type, and one detail object named after the type.
Request
bankUs, accountNumber and routingNumber are required, and accountType defaults to checking. IBAN accounts use a bankIban object (iban and BIC), and Canadian accounts use bankCanada (institutionNumber, transitNumber, accountNumber). Call GET /reference/account-type-requirements?customerId=... to see which types and fields your project accepts for this customer.
Response, 201 Created
pending and moves to active, or to failed or rejected with a failure that says why. Wait for active (or externalAccount.statusChanged) before quoting against it. The full account number is never returned. supportedDestinations lists the rails you can pay this account over.
Reference the ext_ ID as a quote destination to pay out:
usd endorsement to be active. See Send from a wallet for the rest of the flow.
To pay someone other than the customer, register the recipient as a counterparty and the account under it. See Pay a third party. External wallets register through the same endpoint with
type: "walletExternal". Debit cards register only through the card fields of the OMS browser SDK; see Debit cards.What’s next
- Fund the wallet with a cash-in or a virtual account.
- Accept crypto from outside wallets with a deposit address.
- Send funds out with Send from a wallet.