Skip to main content
Total time: about 10 minutes. This page walks the full chain from API key to a transaction.completed webhook. Every code sample uses the themed example data palette; none of the values collide with magic suffixes.

Prerequisites

  • A sandbox API key (ck_sandbox_...). Find yours in the dashboard under API Keys.
  • A webhook endpoint URL. Use webhook.site as a free stand-in for a real endpoint.
  • For the bash track: curl, jq, and uuidgen on your PATH.
Set up the environment once:
Then register the URL as a webhook endpoint — every webhook this page tells you to watch for (application.approved, virtual_account.activated, transaction.*) is delivered only to registered endpoints:
Returns 201 Created. Deliveries to this URL are HMAC-signed; see the Webhooks reference for signature verification.

How the flow works

Step 1 - Upload a KYB document

Onboarding requires at least one supporting business document. Upload a minimal PDF first and capture the returned id for Step 2.
201 Created returns { "id": "doc_...", ... }. Capture the doc_... id as KYB_DOC_ID — Step 2 references it in documentIds. Allowed file types: PDF, PNG, JPEG. Maximum size: 10 MB. The file content (not the filename or Content-Type) is what’s validated.

Step 2 - Onboard the customer

Submit a business onboarding application for Aurora Robotics Inc. with Aiko Tanaka as the beneficial owner. In sandbox the review pipeline is mocked; there is no real KYB call.
Onboarding requirements are dynamic. Call GET /v2/onboarding/requirements?country=USA first to fetch the live { fields[], documents[], minDocuments, individualRequirements[] }. minDocuments is the document floor — when it is 1, attach at least one document before submitting. The body below is one valid US shape, not a fixed contract. Sandbox and production use the same requirements.
Returns 202 Accepted with the application response ({ id: "app_...", status: "processing", type: "customer_onboarding", createdAt, updatedAt, submittedAt, ... }). Capture id as APP_ID. The application is in status: "processing" immediately and the review pipeline picks it up asynchronously. The customerId field is omitted until the application reaches approved.

Step 3 - Approve the application

Drive the application to approved. The synchronous response returns the application ({ id: "app_...", status: "approved", ... }); the new customerId lands a moment later via the application.approved webhook, and on the next GET /v2/applications/{APP_ID}.
Returns 200 with the application object. Your webhook receives application.approved carrying customerId. The id is minted asynchronously, so poll the application until it appears:
Every later step uses this CUSTOMER_ID.
A second simulate/decision call on the same application returns 409 Conflict. First call wins. If you hit 409, fetch the application to confirm its current status before retrying.
You can skip this call entirely. The sandbox auto-approves customer onboarding applications after one hour. Calling simulate/decision is faster for testing.

Step 4 - Create a virtual account feature

Apply for a USD virtual account on the new customer.
202 Accepted returns the application with status: "approved" immediately — feature applications with no extra review payload auto-approve on submission. Provisioning runs asynchronously; your webhook endpoint receives virtual_account.activated { virtualAccountId: vac_... }, and the list call shows the new VA status: "active" within a couple of seconds. Capture it as VAC_ID:
There is no separate simulate/decision approval step for the virtual_account feature. Submitting it without extra review fields auto-approves it inline; calling simulate/decision afterwards returns 409 APPLICATION_ALREADY_DECIDED.

Step 5 (optional crypto branch) - Provision a crypto wallet

Skip this step if you only want the fiat path. The rest of the quickstart (deposit → fiat payout) works without a wallet. New customers reach a usable wallet through a single non-custodial flow: claim non-custodial control → wait for the roster to enroll → the claimed wallets activate automatically. The claim provisions a wallet on every supported chain by default, or just the ones you name in an optional chains array — there is no separate per-chain creation step. Calling POST /v2/customers/:id/wallets before the claim returns 422 WALLET_NO_PROVIDER_ACCOUNT; afterward you only call it to add a chain you excluded from the claim. The custodial path is reserved for legacy customers provisioned before the non-custodial gate and is documented at Custodial vs non-custodial.

Step 5.1 — Enable the CRYPTO_WALLET feature

Before claim-non-custodial, the customer must have an approved CRYPTO_WALLET feature on file. Submit it first:
In sandbox the default is auto-approve: the response carries status: "approved" and you can claim immediately. In live the default is manual review (status: "processing") unless the org has opted out of the review gate; drive a pending application terminal in sandbox via POST /v2/sandbox/applications/:id/simulate/decision { outcome: "approved" }. Calling claim-non-custodial before the feature is approved returns 422 CRYPTO_FEATURE_NOT_APPROVED. Customers whose registered country is on Conduit’s crypto-restricted list get 422 CRYPTO_NOT_AVAILABLE_IN_JURISDICTION at this step (the claim never runs).

Step 5.2 — Claim non-custodial control

POST /v2/customers/:id/wallets/claim-non-custodial provisions the non-custodial wallet account and mints invitations for every roster member. Requires the customer to be KYB-approved AND have an active CRYPTO_WALLET feature row (see Step 5.1). The DTO requires at least 2 roster members and 2 admins; the example below uses a 2-of-3 roster (2 admins + 1 signer, signingThreshold: 2).

Step 5.3 — Enroll the roster

The endpoint returns 202 Accepted with a claimId, rosterSize: 3, and signingThreshold: 2. Each roster member receives an invitation (wallet_signer.invited webhook) and must complete passkey enrollment. The signer rows are minted asynchronously, so the list call below polls until all three appear before driving each headless enrollment:
Once the last signer activates, the wallet account auto-activates and claim.completed fires, carrying the claimId and the activated walletIds (a customer-level crypto_wallet.completed, carrying customerId only, also fires). Verify with GET /v2/customers/:id/wallet-signers (all three rows now status: "active") and GET /v2/customers/:id/wallets (one row per requested chain, each status: "active"; the EVM chains — here ethereum and polygon — share one on-chain address, while solana and tron each get their own). At this point POST /v2/customers/:id/wallets { chain } will succeed for any additional chain you want; called before the customer has claimed non-custodial control, it returns 422 WALLET_NO_PROVIDER_ACCOUNT. See Custodial vs non-custodial for the full mental model.

Step 6 - Fund the customer

Inject a synthetic USD deposit into the virtual account. The deposit completes automatically.
202 Accepted with { externalReference } — the deposit is ingested asynchronously, just as a real bank notification is, so nothing is returned to read an id off. Your webhook endpoint receives transaction.created followed by transaction.completed within a few seconds; to poll instead, use GET /v2/transactions?type=deposit&externalReference=…. The customer’s USD balance is now 1000.00.

Step 7 - Upload a supporting document

Payouts require at least one supporting document. Upload a minimal PDF here and pass its id in the next step.
201 Created. Capture id as $DOC_ID / docId / doc_id.

Step 8 - First payout

A USD payout via FedWire. The account number below has no magic suffix, so compliance clears automatically.
202 Accepted with { "id": "txn_...", "status": "pending" }. Capture the id.

Step 9 - Approve the document review

When a payout’s body includes documents: [...] (and purpose isn’t intercompany), the payout parks at a document-review gate before broadcasting. In production a human reviewer approves the documents; in sandbox you drive that decision with the call below. Without this step the payout sits at status: "pending" indefinitely.
200 OK returns the transaction payload. The payout resumes past the gate, broadcasts the wire, and posts the settlement leg automatically. Within a few seconds GET /v2/transactions/${TXN_ID} shows status: "completed" and your webhook receives transaction.completed.
No separate simulate/settled call is needed in this flow. The sandbox fiat-rail provider settles immediately after the review-approval gate releases. POST /v2/sandbox/payouts/:id/simulate/settled is for payouts that took the no-document path (e.g. purpose: "intercompany" with a whitelisted recipient) and parked at the settlement gate instead of the document-review gate; calling it on a payout that already completed returns 409 CONFLICT.

Verify

The transaction.completed event your endpoint receives has this shape:
status: "completed" confirms the lifecycle is complete. Fee accounting. A FEDWIRE payout carries a fixed fee (here 0.25 USD). The fee is debited from the source on top of the principal: source.assetAmount = principal + fees, so a 25.00 USD recipient credit shows source.assetAmount: "25.25". Branching on source.assetAmount === "25.00" will miss every fee-bearing payout. Either branch on destination.assetAmount (the principal that lands at the recipient) or read fees[] and reconstruct. fedwireImad shape. In sandbox the value is a synthetic 32-character lowercase hex string (e.g. 8c5d129f9f2e47baf76260e03d902e95); in production it follows the standard Fedwire IMAD format (YYMMDDISSSSSSSSC from the originating bank). Both arrive on destination.fedwireImad. The crypto-rail equivalent destination field is txHash.

Where to go next

You’ve completed a full sandbox transaction lifecycle. Explore the per-flow guides for deeper coverage of failure paths, scenario libraries, and all transaction types:
  • Deposits - fiat and crypto deposit simulation, sender-information gate
  • Withdrawals - custodial and non-custodial crypto withdrawals, fiat withdrawals, failure paths
  • Conversions - FX conversion sub-flow, rate-stale and provider-unavailable scenarios
  • Onramps - fiat-in to crypto-out order lifecycle
  • Offramps - crypto-in to fiat-out order lifecycle
  • Custodial vs non-custodial - side-by-side mental model

See also