Skip to main content

Onboarding

Contract §1. Section numbers are contract-wide and never change; the contract overview maps every section to its page.

Conventions — authentication, the response envelope, idempotency, retry safety, pagination, rate limits — are defined in the Partner Integration Guide and apply to every endpoint here. Endpoints marked 🔑 require an Idempotency-Key header.

Onboarding a client, end to end

An end client is a company that gets its own collateral pool and its own cards. Onboarding is a review process, not a call — it runs for hours or days, most of the work happens at the issuer, and your integration's job is to start it, feed it what it asks for, and watch status. Cards cannot be issued until it reaches APPROVED.

Five steps. Step 1 is one call; steps 2 to 4 repeat as the review asks for things.

Step 1 — create the application

POST /clients — a company application: your own reference for the client, the company details, the person opening the account, and whichever representatives and beneficial owners you already know about. The full body is in the endpoint reference below. You get 201 and a client at PENDING.

Two fields are effectively permanent, so get them right on the first call:

  • externalRef is your id for this client and can never be reused — not after a denial, not after a closure. Reusing one answers 409 EXTERNAL_REF_TAKEN.
  • initialUser.walletAddress becomes an admin of the client's collateral contract, and there is no API to add another admin later. If that key is lost the collateral is stranded. See collateral & withdrawal.

Step 2 — send the client to the verification flow

Read GET /clients/:id and take verificationUrl. It is a live field — null on the create response, and null on every row of the list — so only the single-client read produces one.

The end user completes identity and terms there. Until they do the application does not move on its own: NOT_STARTED and TOS_NOT_ACCEPTED both mean waiting for the human, and polling them changes nothing.

Step 3 — watch status, and act only on the states that ask you to

Transitions arrive by webhook (company.updated, company.approved, company.rejected) and are readable by polling GET /clients/:id. Of the eleven states, only four ask anything of you — the status table below is the full list, and the shape of it is:

The state saysYou
NOT_STARTED · TOS_NOT_ACCEPTEDsend the user to verificationUrl (step 2)
NEEDS_INFORMATIONcorrect and resubmit — PATCH /clients/:id, or fix the UBO that is blocking
NEEDS_VERIFICATIONupload the outstanding documents (step 4)
PENDING · IN_REVIEWwait; something is running or a human holds it
APPROVED · EXEMPTproceed — cards may be issued
DENIED · CANCELEDstop; terminal, and the externalRef is spent
LOCKEDpost-approval freeze, not a rejection; contact the programme

Do not treat "not APPROVED" as "pending". NOT_STARTED waits on your user, not on the issuer, and an integration that polls it forever never onboards anybody.

Step 4 — upload what the review asks for

Two separate document surfaces, and a corporate application usually needs both:

  • Company recordsPUT /clients/:id/document. Today only INCORPORATION_CERT and SHAREHOLDER_REGISTRY clear compliance; another accepted type still answers 204 without moving the application forward.
  • Per-UBO identityPUT /clients/:id/ubos/:uboId/document. Without these a corporate application can sit in NEEDS_VERIFICATION indefinitely, which is the most common way an onboarding stalls with nothing visibly wrong.

UBOs themselves are added with POST /clients/:id/ubos — but only while the application is still open. Once the issuer has adjudicated it, an add answers 422 UBO_REJECTED, so submit every beneficial owner before or during review, not after.

Both upload routes are multipart and share one trap: every text part must be sent before the file part. Parts after the file are not read, so a type sent last answers 422 INVALID_DOCUMENT_TYPE on an otherwise valid request.

Step 5 — approved

At APPROVED the client's collateral contract exists — it deploys on approval, no call required — and cards may be issued. From here:

Endpoint reference

The client object

{
"id": "cl_01M0…",
"kind": "COMPANY",
"externalRef": "your-crm-id-123",
"name": null,
"address": null,
"status": "PENDING",
"verificationUrl": null,
"externalVerificationUrl": null,
"ultimateBeneficialOwners": [],
"createdAt": "…",
"updatedAt": "…"
}

verificationUrl and externalVerificationUrl are two separately signed sessions, not a value and its fallback. Use the one you asked for; both are secret, and both are null when the programme supplies no corresponding flow.

ultimateBeneficialOwners[] carries id, firstName, lastName, email, status and verificationUrl per UBO — it is how you find which person is blocking a NEEDS_INFORMATION application. Populated on GET /clients/:id, always [] on the list route, which is served from the service's own rows rather than a live read.

name and address are optional passthrough. They are null when the issuer's application read omits them (the current sandbox shape), and relayed if the issuer adds them.

KYB retention: the exact POST /clients body — dates of birth, identity-document numbers — is retained encrypted at rest (envelope encryption, KMS-held keys) in the permanent operation record, so a submission the programme later disputes can be reconstructed. It is never served through any API and never appears in logs. Everything else the service keeps about your client is identifiers and status mirrors.

Client status

status has eleven values, and the difference between "wait" and "act" is why this table is worth reading rather than treating everything not APPROVED as pending:

StatusWhat you do
NOT_STARTEDSend the client to verificationUrl. Nothing is running; waiting will not move it
PENDINGNothing — automated checks are running
NEEDS_INFORMATIONCorrect and resubmit via PATCH /clients/:id, or read ultimateBeneficialOwners[] to find who is blocking
NEEDS_VERIFICATIONUpload the outstanding documents, or open the hosted flow
IN_REVIEWWait — a human at the programme holds it
TOS_NOT_ACCEPTEDSend the client to verificationUrl. Identity passed; the end user must still accept the programme's terms
APPROVEDCards may be issued
EXEMPTTerminal approval granted manually by the programme. No action
DENIEDTerminal. An externalRef is never reusable
CANCELEDTerminal
LOCKEDPost-approval freeze, not a rejection

NOT_STARTED, TOS_NOT_ACCEPTED and EXEMPT were added 2026-08-22. Treat this list as a floor: switch on the values you handle and fall through to a "needs a human" branch rather than throwing, because the programme can introduce a state on its own timetable.

Transitions arrive by webhook (company.updated, company.approved, company.rejected) and by polling.

POST /clients 🔑

Answers 201 with the client at status PENDING.

externalRef (required, unique per partner) is your stable id for this client and the create's dedup scope, independent of the idempotency key. ⚠ It is stored long-term in plaintext for reconciliation — it must be an opaque identifier from your systems, never personal data (no names, emails, tax ids) and never a secret. Dedup semantics:

  • second create with the same externalRef while the first is still processing409 OPERATION_IN_FLIGHT — wait, then retry the original request with its own key;
  • create reusing an externalRef already used by any of your clients (even a denied or closed one) → 409 EXTERNAL_REF_TAKEN. An externalRef is never reusable.

kind: "COMPANY" body:

{ "kind": "COMPANY", "externalRef": "your-crm-id-123",
"entity": { "name": "…", "registrationNumber": "…", "taxId": "…",
"industry": "541511", // 6-digit NAICS code
"description": "…", "website": "…",
"type": "GmbH", // legal form (optional)
"address": { "line1": "…", "line2": "…", "city": "…", "region": "…",
"postalCode": "…", "countryCode": "DE" } },
"initialUser": { …person…, "role": "DIRECTOR", // role optional, initialUser only
"walletAddress": "0x71C7…976F", // the client's own custody wallet
"ipAddress": "203.0.113.7",
"isTermsOfServiceAccepted": true },
"representatives": [ { …person… } ], // 1–20
"ultimateBeneficialOwners": [ { …person… } ] } // 0–20

…person… (account owner, each representative, each UBO) = firstName, lastName, email, birthDate (YYYY-MM-DD), nationalId, countryOfIssue (alpha-2 of the ISSUING country), phoneCountryCode? (digits, no +), phoneNumber? (digits), address{…}. nationalId is the 9-digit SSN for a US country of issue, otherwise the number on the identity document held for this person. There is no inline document object — identity is verified through the hosted verification flow (verificationUrl) and, where the programme requests documents, through the upload endpoints: company records via PUT /clients/:id/document, per-UBO identity documents via PUT /clients/:id/ubos/:uboId/document. Without the UBO uploads a corporate application can remain in NEEDS_VERIFICATION indefinitely. entity.registrationNumber / taxId / industry / description / website are all required; entity.type is optional. Optional top-level sourceKey (≤24 chars, the programme's origin tag) and entity.expectedSpend are relayed when present. externalRef is ≤191 chars. initialUser.walletAddress (26–128 chars) is the client's own custody wallet — collateral is provider-managed and settles against it.

kind: "INDIVIDUAL" — 🔜 not callable yet. Published for design only: sending it today answers 501 CLIENT_KIND_UNSUPPORTED. Your integration manager announces availability before it ships. The shape, for planning: { "kind": "INDIVIDUAL", "externalRef": …, "person": { …person…, "walletAddress": …, "ipAddress": …, "termsAccepted": true } } — an individual is provisioned as a single-person program account.

isTermsOfServiceAccepted: true is required on the initial user — you attest your client accepted the program terms.

GET /clients · GET /clients/:id

The LIST is offset-paginated with a real pagination block (totalObject, totalPages), because it is served from the service's own records. _limit defaults to 20, caps at 100; _page starts at 1. Two consequences:

  • status on the list is the mirrored status, kept current by webhooks, so it can lag a transition by one delivery. GET /clients/:id reads through to the issuer; the list does not, because one issuer call per row would make paging the thing that rate-limits you. If that read fails, the single-client route falls back to the mirror rather than erroring — treat it as freshest-available, and re-read before acting on a status you must be sure of.
  • verificationUrl is null on every row of the list — it is a live field. Read the client itself to collect one.

PUT /clients/:id/document

multipart/form-data — the one exception to this API's JSON rule. One file per call, max 20 MB.

PartReqValue
documentthe file itself
typeone of the 12 values below
namea label for the document
sideFRONT or BACK
countryCodealpha-2

Send every text part BEFORE the file part. Parts that arrive after the file are not read, so a type sent last answers 422 INVALID_DOCUMENT_TYPE on a request that is otherwise perfectly valid. The same ordering rule governs PUT /clients/:id/ubos/:uboId/document.

The 12 accepted type values:

INCORPORATION_CERT SHAREHOLDER_REGISTRY DIRECTORS_REGISTRY
STATE_REGISTRY INCUMBENCY_CERT INCORPORATION_ARTICLES
GOOD_STANDING_CERT TRUST_AGREEMENT INFORMATION_STATEMENT
POWER_OF_ATTORNEY PROOF_OF_ADDRESS OTHER

No Idempotency-Key: uploads are safe to repeat — the application is reviewed against the document set as a whole. Acceptance/rejection is reflected in client status; there is no per-document receipt or body, and no document id, because the service stores nothing. The bytes are relayed to the programme and never written down. A successful request is an empty 204.

The programme parses what you send. Its verification vendor's documented rejection tags are returned in error.params.tags and map to 422 DOCUMENT_QUALITY_REJECTED (retake the photo), DOCUMENT_REJECTED (send a different document), or DOCUMENT_INCOMPLETE (send the missing page or side). An unrecognised tag safely maps to DOCUMENT_REJECTED. Request validation still uses INVALID_DOCUMENT_TYPE; oversized files answer 413 DOCUMENT_TOO_LARGE.

All 12 type values are relayed because issuer requirements can change. Today only INCORPORATION_CERT and SHAREHOLDER_REGISTRY clear company compliance; another accepted upload can still return 204 without moving the application toward approval.

UBO write endpoints

POST /clients/:id/ubos adds a UBO and returns the refreshed client; its body is the §1 person shape except lastName is optional (single-name persons are accepted). PATCH /clients/:id/ubos/:uboId updates one UBO, and PUT /clients/:id/ubos/:uboId/document relays a person identity document, 204. The uboId is the issuer's identifier, checked against the requested client before any write.

UBO writes are accepted only while the application is still open — once the issuer has adjudicated it (APPROVED/DENIED) an add answers 422 UBO_REJECTED ("not in a valid state to add a beneficiary", measured live 2026-08-26), so submit UBOs before or during review. A repeated POST adds another UBO, so it takes no Idempotency-Key: after an ambiguous response, read the client before retrying.

The 19 accepted UBO document types — a separate enum from the company one above:

PASSPORT ID_CARD DRIVERS
RESIDENCE_PERMIT UTILITY_BILL SELFIE
VIDEO_SELFIE PROFILE_IMAGE ID_DOC_PHOTO
AGREEMENT CONTRACT DRIVERS_TRANSLATION
INVESTOR_DOC INCOME_SOURCE PAYMENT_METHOD
BANK_CARD VEHICLE_REGISTRATION_CERTIFICATE
COVID_VACCINATION_FORM OTHER

For PASSPORT, omit side — the side selector applies only to cards, driver's licences and residence permits.

PATCH /clients/:id

Amends the company application — the answer to NEEDS_INFORMATION. Only the issuer-supported entity fields and a complete address object may be sent; externalRef is immutable.

The issuer restarts verification after an amendment, so the status you had is stale the moment the call returns: read the client again before resuming any polling loop.