This page walks the full chain from API key to a
transaction.completed
webhook. Most code samples use the example data palette
— it’s illustrative test data, not a required value, so substitute your own
freely; the signing-roster steps below use their own example emails instead.Prerequisites
- A sandbox API key (
ck_sandbox_...). Find yours in the dashboard under API Keys. - A webhook endpoint URL. Use webhook.cool 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 — an example business from the example data palette; use your own values freely. To onboard a natural person instead, follow the Individual quickstart. 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.The requirements endpoint is authoritative for the industry field name and its accepted values — they are jurisdiction-specific, so read them from the response rather than hard-coding. For USA it returns companyClassification.primaryIndustry as an enum whose values are catalog keys from GET /v2/onboarding/policy-subjects?axis=industry ("fintech_companies", "manufacturing", …); send a key, not a display name. The sample below uses primaryIndustry; read the fields from the requirements endpoint rather than hard-coding them.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. The customerId is minted asynchronously; your registered webhook endpoint receives application.approved carrying it — open your webhook.cool inbox (the WEBHOOK_URL from the prerequisites), find the delivery, and copy data.customerId into CUSTOMER_ID:
bash
CUSTOMER_ID.
A business application does not auto-approve. After one hour the sandbox
clears the mock KYC checks, and the application then waits for
simulate/decision; if nothing decides it within 14 days it cancels on its
own, and simulate/decision still decides a cancelled application. An
individual application whose attestation matches, with the ID document
attached, approves with no call.Step 4 - Apply for 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, in both sandbox and production. Provisioning runs asynchronously; your webhook endpoint receives virtual_account.activated { virtualAccountId: vac_... } — open your webhook.cool inbox, find the delivery, and copy data.virtualAccountId into VAC_ID:
bash
Step 5 - 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. The customer’s USD balance is now 1000.00.
Step 6 - 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 7 - 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 8 - Approve the document review
If your payout’spurpose needs a supporting document, it waits for a document review before anyone signs — whether or not you already attached one. In production a human reviewer approves it; in sandbox, approve it yourself with the call below. A payout that attached its document stays pending until approved; one that attached none is asked for the document instead, and that request expires after 48 hours.
200 OK returns the transaction payload. The payout resumes past the gate, broadcasts the wire, and settles automatically. Within a few seconds GET /v2/transactions/${TXN_ID} shows status: "completed" and your webhook receives transaction.completed.
No
simulate/settled call is needed here — the sandbox fiat-rail provider
settles automatically once the payout clears review. Calling POST /v2/sandbox/payouts/:id/simulate/settled on an already-completed payout
returns 409 CONFLICT.Step 9 (optional crypto branch) - Provision a crypto wallet and send a testnet payout
Optional — skip this section if you only wanted the fiat path above. It reuses theCUSTOMER_ID from Step 3 to provision a non-custodial wallet, fund it from a real testnet, and send a crypto payout from it.
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 each default chain that settles on sandbox (ethereum, base, polygon; the default tron does not settle here, so it is left out), 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 outside the default set (such as solana) or one you excluded from the claim.
Step 9.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"); 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 9.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 approved AND have an approved CRYPTO_WALLET feature (see Step 9.1).
A business roster needs at least 2 members and 2 admins. The roster shape you send depends on the customer’s signing mode, so send the one that matches — the wrong shape is rejected at claim. Read the mode before you build the roster: for a customer with no wallet claim yet, GET /v2/customers/:id returns the current signingMode, the value this claim would freeze. Once a customer has claimed, its wallets keep the mode they froze at claim time even if signingMode changes afterward.
Sandbox default:
programmatic_unattended. A new sandbox organization resolves to the programmatic_unattended signing mode. That mode permits api_key admins, so the minimal machine roster is two api_key admins reaching the threshold — the machine-signer roster shown below (each with a compressed publicKey, signingThreshold: 2). The mode never limits passkeys: the all-passkey roster shown after it is valid in every mode, and passkeys count toward signingThreshold. (The stricter programmatic mode rejects api_key admins — there the machine signers take role: "signer" and admins are passkeys; 422 SIGNING_MODE_ROSTER_INVALID otherwise.)Conduit sets the signing mode per organization, with an optional per-customer override (new sandbox organizations get programmatic_unattended); it is not a self-serve field. Before claiming, read GET /v2/customers/:id → signingMode for the value this claim will freeze. A payout’s transaction.awaiting_signature webhook also carries signingMode — the mode the wallet froze at claim time, which can differ from the customer’s current signingMode if the organization’s setting changed since. If a customer resolves to passkey_required instead — an organization or customer with an explicit override does — use the passkey roster variant shown after the machine-signer example below, because that mode refuses any api_key member.Machine (api_key) roster members need no enrollment ceremony, so the wallets activate on their own shortly after the claim — see claim.completed below — and you skip Step 9.3. You then approve each payout with a machine stamp; see Machine-signer stamping for generating the keypair and stamping.signingThreshold: 2), the minimal all-machine roster under programmatic_unattended. Each member’s publicKey is a compressed key you generate and hold. Omit signatureScheme, as below, and the key is a P-256 (secp256r1) one; set it to secp256k1 to register a secp256k1 key that signs the same SHA-256 digest, or to secp256k1_eip191 for a secp256k1 key that approves with an EVM personal_sign. See Machine-signer stamping § Generate a machine signing keypair.
publicKey is the compressed public half of a keypair you generate and hold — P-256 here, because these members send no signatureScheme; the two above are examples, substitute your own. Conduit never holds the private half. Every roster member needs a unique email — including api_key machine signers — because email is the member’s dedupe key. Because both members are api_key, the roster reaches its 2-of-2 quorum with machine stamps and there is no passkey enrollment step. The claim returns 202 with status: "provisioning"; activation then completes on its own. Await claim.completed, which carries the claimId and the activated walletIds, then skip Step 9.3. To sign a payout later, stamp the signingRequestId from the transaction.awaiting_signature webhook at POST /v2/signing-requests/:id/approve — full loop in Machine-signer stamping.
Passkey roster variant (every signing mode)
Passkey roster variant (every signing mode)
Every signing mode accepts an all-This all-
passkey roster, and it is the only choice when your customer resolves to passkey_required (Conduit sets this per organization, with an optional per-customer override; an organization or customer with an explicit override resolves here) — exactly 2 admins for a business customer. Each member then enrols a passkey before the wallets activate, so follow Step 9.3.passkey roster is valid in every signing mode, including programmatic_unattended (the sandbox default). Each payout then waits for passkey approvals on the Conduit-hosted verification page.Step 9.3 — Enroll the roster (passkey roster only)
Machine-signer roster? Skip this step.
api_key members need no passkey enrollment, so activation completes on its own: the claim returns status: "provisioning" (rosterSize: 2, signingThreshold: 2), then claim.completed fires as described in Step 9.2 — continue to Step 9.4 once it does.202 Accepted with a claimId, rosterSize: 3, and signingThreshold: 2. Each roster member receives a wallet_signer.invited webhook with their own verificationUrl. Each signer opens that URL on their device and registers a passkey: admins register two passkeys — the second is for account recovery — and signers register one. wallet_signer.enrolled fires for each signer when enrollment completes. A passkey needs a person at a device, so this step cannot run from a script. For a headless run, use the machine-signer roster above.
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 } succeeds for any additional supported chain except tempo, which holds custodial wallets only and returns 422 NON_CUSTODIAL_CHAIN_NOT_SUPPORTED. A stellar wallet reads pending until the signers approve its one-time on-chain setup (Stellar setup); called before the customer has claimed non-custodial control, it returns 422 WALLET_NO_PROVIDER_ACCOUNT.
Step 9.4 — Fund the wallet from a testnet faucet
The deployed sandbox runs a real public testnet, so a transfer from this wallet broadcasts for real — crypto deposit simulation routes are unavailable here. Send real test tokens to the walletaddress (Step 5 funded the fiat virtual account, not this wallet). See Use a real testnet § Fund the wallet from a faucet for the faucet links per chain. Capture the parked deposit’s id from your webhook endpoint’s transaction.awaiting_sender_information delivery as DEPOSIT_TXN_ID.
Step 9.5 — Clear the sender-information gate
A faucet sends from an address the customer never registered, so the deposit parksawaiting_sender_information until you clear it. register: true registers the address against the customer, so later deposits from it pass without parking:
transaction.completed for it.
Step 9.6 — Send a testnet payout
Send a crypto payout from the funded wallet the same way Withdrawals describes for a non-custodial wallet: the samePOST /v2/payouts endpoint, with a destination: { type: "wallet", ... } crypto destination. Wait for transaction.processing, then transaction.awaiting_signature if the roster has not signed yet, then transaction.completed once the broadcast settles — no polling needed either way.
Step 9.7 — Sign the payout
A machine-signer roster (the Step 9.2 default) signs on its own: stamp thesigningRequestId from transaction.awaiting_signature at POST /v2/signing-requests/:id/approve, as in Machine-signer stamping. A passkey roster needs a person: a signer opens the verificationUrl from that same webhook and approves with their passkey. Either way, transaction.completed confirms the payout reached the chain.
Verify
Thetransaction.completed event your endpoint receives has this shape:
status: "completed" confirms the lifecycle is complete.
Fee accounting. Branch on destination.assetAmount for what reached the destination and read fees[] for the charge — don’t assume which side carries the principal. Full rules, including how a Conduit-invoiced fee is reported without moving either side’s amount: transaction.completed.
fedwireImad shape. In sandbox the value is a synthetic 32-character lowercase hex string (e.g. 8c5d129f9f2e47baf76260e03d902e95); in production it is a 22-character Fedwire IMAD — an 8-digit input date (YYYYMMDD) followed by 14 more characters from the originating bank (e.g. 20260616MMQFMP9C000123). 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 simulation, real testnet wallet funding, sender-information gate
- Withdrawals - crypto and 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