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, anduuidgenon your PATH.
application.approved, virtual_account.activated, transaction.*) is delivered only to registered endpoints:
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 returnedid 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.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 toapproved. 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}.
200 with the application object. Your webhook receives application.approved carrying customerId. The id is minted asynchronously, so poll the application until it appears:
CUSTOMER_ID.
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 optionalchains 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 approvedCRYPTO_WALLET feature on file. Submit it first:
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 returns202 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:
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 itsid 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 includesdocuments: [...] (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
Thetransaction.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