Overview
In the sandbox environment, customer KYC is mocked: no upstream KYC provider is called, and no end-user KYC data is collected. Customer onboarding still flows through the full review pipeline (sourcing, document validation, extraction, verification, compliance checks) — but the compliance-checks stage is satisfied by a mock that defers the verdict for one hour by default. Org-level KYB is not mocked. Your organization completes real KYB against real providers in sandbox; sandbox KYB approval is the prerequisite for graduating to production.Customer-onboarding lifecycle
When you submit a customer onboarding application in sandbox:- The application enters
"processing". - The mock records
PENDINGfield verifications for the same paths a real KYC transaction would (business website, classification report, adverse-media report, associated persons, TIN — depending on the submitted data and country). - A 1-hour timer starts.
- Either you call one of the simulate endpoints below to drive a specific outcome, or the timer fires and auto-approves the application.
409 Conflict.
Simulate endpoints
POST /v2/sandbox/applications/:id/simulate/decision forces a sandbox application to a terminal status on demand, regardless of the application’s current review stage. The body’s outcome field selects approved or rejected.
Works for any application type
This endpoint is not limited to customer onboarding. It accepts every application type your sandbox API key can create:| Application type | On outcome: "approved" |
|---|---|
CUSTOMER_ONBOARDING | Customer transitions to ACTIVE and customer.activated fires. |
VIRTUAL_ACCOUNT | The virtual account and its underlying account details are provisioned in one shot (see Virtual accounts for the end-to-end shape). |
CRYPTO_WALLET | The customer’s crypto provider account is provisioned. Use POST /v2/customers/:id/wallets {chain} to create each chain’s wallet. |
WALLET_CUSTODY_CONVERSION | The customer’s wallets flip to custodyModel: "non_custodial" and the cosign gate becomes live for future payouts (see Non-Custodial Wallets). |
CUSTOMER_UPDATE | The pending customer update is applied. |
outcome: "rejected" publishes the matching *.rejected event for the application’s type. Rejected applications never reach a provisioning branch.
Request shape
The endpoint requires your sandbox API key and a JSON body with anoutcome field. The simplest approval call:
200 OK with the application at its current state. Replays against an already-terminal application return 200 with the current resource (idempotent).
Rejection options
The same endpoint withoutcome: "rejected" accepts optional fields to shape the rejection:
| Body | Behavior |
|---|---|
{ "outcome": "rejected" } | Generic rejection. The persisted reason falls back to a sentinel string. |
{ "outcome": "rejected", "reason": "<text>" } | Free-text rejection. The string is persisted verbatim as the rejection reason. |
{ "outcome": "rejected", "category": "...", "field": "..." } | Structured rejection that drives the rejection down a specific sub-path so the persisted reason matches what the live KYB pipeline would produce. |
category is only meaningful for KYB-pipeline application types (CUSTOMER_ONBOARDING, ORGANIZATION_ONBOARDING, CUSTOMER_UPDATE, SANCTIONS_REVIEW). Sending category on a non-KYB type (e.g. a VIRTUAL_ACCOUNT application) returns 422 REJECTION_CATEGORY_NOT_APPLICABLE. Sending category with outcome: "approved" returns 400 VALIDATION_ERROR. Within category: GENERIC an optional reason is accepted (this is how integrators simulate a manual operator rejection); for all other categories reason cannot be combined with category.
Categories: DOCUMENT_MISMATCH (optional field: TAX_ID, DATE_OF_INCORPORATION, BUSINESS_NAME, BUSINESS_ENTITY_ID), DATA_SOURCING_MISMATCH (optional field: BUSINESS_ENTITY_ID, UBO_FIRST_NAME, UBO_LAST_NAME, UBO_PHONE, UBO_EMAIL, OWNERSHIP_LIST), COMPLIANCE (no field), GENERIC (no field; accepts an optional reason).
Errors
| Status | Reason |
|---|---|
400 | Missing outcome, non-object body, or reason combined with a category other than GENERIC. Also returned when category is sent with outcome: "approved". |
401 | Missing or invalid x-api-key. |
404 | The application does not exist or does not belong to your organization. |
422 | category was sent against a non-KYB application type (VIRTUAL_ACCOUNT, CRYPTO_WALLET, WALLET_CUSTODY_CONVERSION). |
Non-custodial wallet provisioning
A wallet is created custodial by default. To flip a wallet to non-custodial, create aWALLET_CUSTODY_CONVERSION feature application after the CRYPTO_WALLET feature is approved.
See the Custodial vs non-custodial guide for the full copy-paste curl flow with both custody models walked side by side.
Notes
- Customer KYC data submitted to sandbox is not transferred to production. Sandbox and production are isolated environments — end-user customers re-onboard in production.
- The mock matches the real KYC adapter’s field-path set so your client code sees the same shape on
application_field_verificationswhether it’s running against sandbox or live. - The 1-hour fallback applies to customer-onboarding applications only and makes it safe to ignore simulation entirely for happy-path tests — submit, wait, observe an
approvedoutcome. Other application types do not auto-resolve; drive them withsimulate/decision { outcome: "approved" | "rejected" }.