Skip to main content
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.
This guide takes one end user from nothing to a customer who can hold stablecoins and receive bank payouts: create the customer, wait for onboarding to finish, provision a custodial wallet, and register a bank account. Every transaction, deposit address, and virtual account belongs to a customer, so this is where an integration starts. For what a customer and its endorsements are, see Customers and Entities.

Flow

Customer onboarding flow
1User→AppIdentity details
2App→OMSPOST /customers
3OMS→App{id: “cst_…”, status: “pending”}
4OMS→AppWebhook: customer.statusChanged (active)
5App→OMSPOST /wallets
6OMS→App{id: “wlt_…”, address: “0x…“}
7App→OMSPOST /external-accounts
8OMS→App{id: “ext_…”, status: “pending”}

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.statusChanged so you learn when onboarding finishes. See Receiving webhooks.

Step 1: Create the customer

Send POST /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:
    • email
    • birthDate as YYYY-MM-DD
    • phone as a US number in E.164 form: +1 followed by 10 digits
    • identifyingInformation with an ssn entry: { "type": "ssn", "issuingCountry": "US", "number": "123456789" }
    • residentialAddress in the US, with line1, city, a two-letter state, zipCode, and country: "US". Addresses in New York, Texas, and Vermont are not supported on these projects.
  • If your project does not verify customers, type plus 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.
Do not send 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

Pass your own user ID as 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.
In sandbox, avoid the word “test” in names and email addresses. Fraud checks can flag them and stop the customer reaching active.

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:
Until creation finishes, 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 as 400 with code 2001 (InvalidArgument). The msg is always "invalid argument"; the reason is in cause:
  • cause names a field that “is not yet supported”: remove that field and send again. These come back one field per response.
  • cause lists required fields (for example ssn is required): your project verifies customers. Add the fields listed in Which fields to send. A cause can hold several reasons separated by newlines.
  • Anything you cannot resolve: every response carries a Request-Id header. Log it and include it when you contact support.

Step 2: Wait for the customer to become active

Onboarding runs after creation. Watch for customer.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 with type: "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:
If your project has business onboarding enabled, where you verify the business yourself and attest to it, register the people behind it and submit an onboarding:
1

Add each beneficiary

POST /customers/{customerId}/beneficiaries once per person. roles is required (beneficialOwner, controller, director, signatory, or representative).
Store the returned beneficiary 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.
If business onboarding is not enabled for your project, contact us to enable it. For the full beneficiary model, see Business onboarding.

Step 3: Provision a custodial wallet

Once the customer’s cryptoCustody 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 Created when the wallet already exists or is confirmed during the request. The body is the wallet.
  • 202 Accepted when the custody provider confirms later. The body is a wallet operation, and the Location header points at GET /wallets/operations/{operationId}. Poll it until status is complete and read walletId, or wait for customerWallet.statusChanged.
A 201 body, abridged (the full wallet also carries provider, label, and metadata):
Requests use 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 with POST /external-accounts. The body carries an owner, a type, and one detail object named after the type.

Request

For 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

The account starts 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:
Bank payouts need the customer’s 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