simulate/* endpoints. Crypto broadcast and signing are real. See Sandbox overview for the full posture.
Crypto withdrawal on real testnet
A non-custodial crypto payout onPOST /v2/payouts behaves like production:
- It broadcasts for real. The
wallet_to_extleg sends on the wallet’s testnet (Sepolia, Base Sepolia, Amoy, Solana Devnet), andtransaction.completedcarries a realtxHashyou can open on that network’s block explorer. - It needs a real signature. The payout parks at
transaction.awaiting_signature. Apasskey_requiredwallet approves through the webhook’sverificationUrl; a programmatic wallet carries asigningRequestIdyour backend stamps atPOST /v2/signing-requests/:id/approve(Machine-signer stamping). ThePOST /v2/sandbox/payouts/:id/simulate/cosignandsimulate-stamplevers return409 SANDBOX_SIGNING_SIMULATION_UNAVAILABLE— there is no shortcut. (A wallet provisioned before real testnet was enabled is refused at creation with422 LEGACY_MOCK_WALLET_UNSUPPORTED.) - Fund the source wallet from a faucet. Send real test tokens to the wallet address, then withdraw. See Use a real testnet for faucets, finality thresholds, and explorers.
- It takes minutes to reach finality. Poll
GET /v2/payouts/:idor watchtransaction.completed.
- Crypto withdrawal: real testnet broadcast, signed by the customer’s roster; finality takes minutes.
- Fiat withdrawal: bank-recipient payout,
payouts/:id/simulate/settleddrives settlement.
Withdrawal state machine
Annotations: a payout the documentation policy requires a document for parks for document review during compliance screening, before the customer is asked to sign — callpayouts/:id/simulate-review-approve to resume it, or simulate-review-reject to terminate it as failed with failureCode: compliance_rejected (the reserved funds return to your available balance). A payout the documentation policy requires no document for (intercompany payouts, which use a whitelist recipient, and by default prefunding payouts) skips the gate. A payout policy requires a document for but that attached none also parks here, waiting on you instead of on an analyst. Because document review is part of the pre-signing compliance stage, the payout reads as status: "pending" while parked here — it flips to "processing" only once compliance + Travel Rule clear (transaction.processing). Travel Rule counterparty-webhook autopilot fires 10 s after the Travel Rule transfer is created on suffixes that encode a counterparty outcome — that transfer is created once compliance + document review clear, not at payout creation — see Travel Rule scenarios. Crypto payouts reach completed at real testnet finality. Fiat payouts use payouts/:id/simulate/settled to terminalize. Transaction-level transactions/:id/simulate/terminal { outcome: "failed" } can force accepted fiat payouts to failed earlier, once the document-review gate is clear; outcome: "completed" requires settlement-ready state. The transaction-level endpoint supports withdrawal, onramp, offramp, and deposit transaction types.
Prerequisites
- A sandbox API key for an
activecustomer (the onboarding flow leaves the customer in this state). SetSANDBOX_API_KEYin your shell. - The customer must have an
activecrypto wallet for the asset and chain you want to test. New customers provision non-custodial wallets viaPOST /v2/customers/:id/wallets/claim-non-custodial; without that claim,POST /v2/customers/:id/walletsrejects with422 WALLET_NO_PROVIDER_ACCOUNT. - For fiat withdrawals: the customer must have an
activevirtual account with a sufficient USD balance. - Base URL:
https://api.sandbox.conduit.financial.
curl examples below use {{apiKey}}, {{customerId}}, {{walletId}}, and {{payoutId}} placeholders. Substitute the values from your sandbox setup.
Lifecycle
A sandbox payout walks the same lifecycle as production: validate → reserve → compliance screen → travel-rule resolve → (document review if policy requires a document) → queue → collect the customer’s signatures → final co-sign → broadcast → await finality → settle. The full compliance screen and Travel Rule run before the customer is asked to sign, so screening happens before any signature is requested — the customer never signs a payment that then fails screening. A compliance rejection does not stop the payout automatically: it is held for review and then either released to proceed or confirmed as terminal (rejected or frozen); on a confirmed rejection the payout never broadcasts. The lifecycle is identical to production; only the decisions differ:Full happy path: non-custodial wallet (single-signer cosign)
This flow covers a non-custodial wallet with a signing threshold of 1: its roster still has the required minimum of two admins, but a single signer’s stamp clears each payout. Provisioned viaPOST /v2/customers/:id/wallets/claim-non-custodial. For higher M-of-N thresholds, use the multi-signer flow below — claim-non-custodial is the only new-customer entry point either way.
Once the wallet is active, every payout from it is screened for compliance and Travel Rule first; only once it clears does the payout pause to collect the customer’s signatures before it is finalized. A passkey signer approves on the Conduit-hosted verify page, and a machine signer stamps the signing request, in sandbox as in production.
Step 1 — Create the wallet
Once the customer has claimed non-custodial control viaPOST /v2/customers/:customerId/wallets/claim-non-custodial and the roster has enrolled (each passkey signer through the verificationUrl on wallet_signer.invited; a machine roster needs no enrollment), POST /v2/customers/:customerId/wallets with { "chain": "ethereum" } returns 201 Created and a wallet object with id, address, chain, status: "active", and custodyModel: "non_custodial". Capture id as {{walletId}}. Before the customer has claimed non-custodial control, the same call returns 422 WALLET_NO_PROVIDER_ACCOUNT.
Step 2 — Fund the wallet
Send real testnet funds from a faucet to the wallet address. Wait for deposit finality before creating the payout.Step 3 — Create the payout
See the multi-signer flow below for the fullPOST /v2/payouts request body (USDC on ETHEREUM, crypto destination with attestation.custody: "self", plus documents and purpose) — the request shape is identical between the two flows. The response carries requiresUserSignature: true:
queuePosition is not in the create response. When the payout is queued behind earlier signing work on the same wallet+chain, it enters the queue gate just after the 202 and Conduit emits transaction.queue_position_changed carrying its place — a positive integer (1 is next in line) — on entry and on each move forward. The same value is available on GET /v2/payouts/:id. The head (active) payout — the one currently being signed — carries no place.
transaction.awaiting_signature fires when the payout parks at the cosign gate, and the webhook payload carries verificationUrl + expiresAt. Subscribe to that topic to receive the verify URL — it is not part of the GET response shape. This applies to a wallet in the passkey signing mode; a wallet in a programmatic signing mode instead carries a signingRequestId on that webhook — see Machine-signer stamping.
Step 4 — Approve the document review
Because the documentation policy requires a document for thispurpose, the payout parks for document review during compliance screening — before the customer is ever asked to sign. Clear it first via the sandbox simulate endpoint:
200 OK. Call simulate-review-reject instead to terminate the payout as failed with failureCode: compliance_rejected (reserved funds returned). Once compliance and Travel Rule clear, transaction.processing fires (status becomes processing) and the payout parks at the cosign gate — transaction.awaiting_signature fires with the verificationUrl.
Step 5 — Approve the payout
A signer opens theverificationUrl from transaction.awaiting_signature and approves with their passkey. A programmatic wallet stamps the signingRequestId at POST /v2/signing-requests/:id/approve instead (Machine-signer stamping). The payout then broadcasts on the testnet and reaches status: completed at finality (minutes). A decline on the approval page fails the payout with failureCode: "user_signature_declined". POST /v2/sandbox/payouts/{payoutId}/simulate/cosign returns 409 SANDBOX_SIGNING_SIMULATION_UNAVAILABLE.
Force-fail frees the wallet immediately. Force-failing a payout parked at the cosign gate (via
POST /v2/sandbox/transactions/:id/simulate/terminal { outcome: "failed" }) frees the wallet at once: a follow-up payout on the same wallet is accepted within milliseconds, with no cooldown.Full happy path: multi-signer non-custodial
When the wallet was provisioned viaPOST /v2/customers/{id}/wallets/claim-non-custodial, every payout pauses at an M-of-N quorum gate instead of a single-signer cosign gate. Each signer stamps independently; the payout auto-broadcasts when the threshold is met.
For the mental model see Multi-signer wallets. For threshold rules see Signing thresholds. For the copy-paste walkthrough see Multi-signer wallets recipe.
Step 1 — Provision the roster
CallPOST /v2/customers/{customerId}/wallets/claim-non-custodial with a roster (admins + signers, each with a unique email and credentialType: "passkey") and a signingThreshold. The endpoint returns 202 with a claimId and dispatches one wallet_signer.invited webhook per roster member.
Step 2 — Enroll each signer
Each signer opens theverificationUrl from their wallet_signer.invited payload and enrolls a passkey on their device, in sandbox as in production. Once the final signer is enrolled, crypto_wallet.completed fires and the wallet flips active.
Step 3 — Fund the wallet
Send real testnet funds from a faucet to the wallet address. Wait for deposit finality before creating the payout.Step 4 — Create the payout
202 Accepted with { "id": "txn_...", "status": "pending", "requiresUserSignature": true, ... }. Capture the id as {{payoutId}}. The documents array must contain the id from a prior POST /v2/documents upload — a payout without one is either refused with 422 DOCUMENTATION_REQUIRED or accepted and parked at the document-review gate as CUSTOMER / DOCUMENT_REQUESTED, with a request for information opened against it, set per organization. Because the documentation policy requires a document for this purpose, the payout first parks for document review during the pre-signing compliance stage (Step 5). Only once that clears (and Travel Rule passes) does transaction.processing fire and the payout park at the quorum gate, at which point transaction.awaiting_signature fires with verificationUrl and expiresAt.
Step 5 — Approve the document review
The documentation policy requires a document for thispurpose, so the payout parks for document review during the pre-signing compliance stage — before the signers are asked to stamp. Clear it first:
200 OK. Call simulate-review-reject instead to terminate the payout as failed with failureCode: compliance_rejected (reserved funds returned). After approval, compliance + Travel Rule finish, transaction.processing fires, and the quorum gate opens.
Step 6 — Collect stamps to quorum
Each signer opens theverificationUrl from transaction.awaiting_signature on the device where they enrolled, and approves with their passkey. Quorum progress comes from the transaction.signature_collected webhook, whose payload carries the running collected and required counts. Once collected reaches required, transaction.quorum_met fires and the payout proceeds to broadcast. POST /v2/sandbox/payouts/{payoutId}/simulate-stamp returns 409 SANDBOX_SIGNING_SIMULATION_UNAVAILABLE on the sandbox.
Rejection. A single decline terminalizes the quorum and fails the payout with failureCode: "user_signature_declined". The remaining signers cannot un-reject.
Re-stamping. A second approval by the same signer does not double-count, and transaction.signature_collected reports the same collected count.
Ghost-vote scrubbing. If a signer is removed from the roster mid-payout (via DELETE /v2/customers/:id/wallet-signers/:signerId), their already-cast stamps are scrubbed from every in-flight payout for the customer. Affected payouts re-fire transaction.signature_collected with the new count; payouts that can no longer reach quorum on the new roster terminate with failureCode: "roster_changed". See Ghost-vote scrubbing for the walkthrough.
Once collected reaches required, transaction.quorum_met fires, the payout broadcasts on the testnet, and it reaches completed at real chain finality (minutes).
Fiat withdrawal
A fiat withdrawal moves funds from a customer’s virtual account to a bank account. The payout is submitted to the configured payment rail; in sandbox the rail call is mocked andsimulate/settled drives the terminal outcome.
Step 1 — Create the fiat payout
202 Accepted with { "id": "txn_...", "status": "pending", ... }. Capture the id as {{payoutId}}. Compliance screening runs automatically; in sandbox it resolves based on the account-number suffix (see catalog below). The accountNumber 1234594000000 ends in the last 8 digits 94000000 which is the happy-path suffix. Because the documentation policy requires a document for this purpose, the payout then parks for document review (status: "processing") before settlement — clear the gate in the next step.
purpose and documents are required. Every payout except purpose: intercompany and, by default, purpose: prefunding needs at least one documents entry to take this happy path (documentation policy can still require documents on a prefunding payout above a configured amount). An organization set to POST_ACCEPTANCE instead accepts the payout without one and asks for the document at the review gate. Upload a supporting document first (POST /v2/documents) and pass its id in the documents array. intercompany payouts use a registered whitelist recipient instead of a document — see Whitelist Recipients. A prefunding payout must also be sent from a house account: your sandbox organization is seeded with one, or mark a BUSINESS customer you onboarded yourself with PATCH /v2/sandbox/customers/{customerId}/house-account.Step 2 — Approve the document review (sandbox only)
A fiat payout the documentation policy requires a document for parks for document review before it is submitted to the rail, exactly like the crypto flow. Approve it to let settlement proceed:200 OK returning the payout at its current state. Call simulate-review-reject instead to terminate the payout as failed with failureCode: compliance_rejected; transaction.rejected fires and the reserved funds return to the virtual account.
Step 3 — Drive settlement
200 OK returning the payout at its current state. The outcome field is "completed" or "failed". utr is required when outcome is "completed" — supply a non-empty bank reference (IMAD, UETR, ACH trace number, or instant-payment reference). The payout transitions to status: "completed", completedAt is populated, and transaction.completed fires carrying that reference on the destination’s typed wire-reference field for the rail (e.g. external_bank.fedwireImad, external_bank.swiftUetr).
To drive a failure instead, use outcome: "failed" with an optional reason string:
transaction.failed fires with rail_unavailable or the failure code from the rail mock.
Fiat account-number suffix catalog
The sandbox reads the last 8 digits ofrecipient.accountNumber (all non-digit characters stripped before matching) to select a deterministic outcome. Set the suffix at account-creation time by choosing an accountNumber whose tail matches the desired scenario.
Accounts not matching any documented suffix take the happy path (
94000000 behavior).
Routing-number directory
On a US rail (fedwire, rtp, fednow, ach), destination.recipient.bankAddress is optional. Omit it and Conduit derives the recipient bank’s name and address from routingNumber.
In sandbox every routing number resolves to Simulated National Bank, except 999999992, which the directory does not hold. An unheld routing number returns 400 VALIDATION_ERROR with pointer /destination/recipient/bankAddress, exactly as production does for a routing number the real directory does not hold. When the directory cannot answer at all, the payout returns 503 RECIPIENT_BANK_LOOKUP_UNAVAILABLE, which is retryable and says nothing about the routing number you sent.
A request that supplies both bankAddress and bankName never consults the directory. Supplying bankAddress alone still consults it to fill the missing bankName, and the payout still goes through when the directory cannot answer, because bankName is optional. A bankName you send is never overwritten.
Choose the account-number suffix for the scenario you need:
94009001–94009003produce a bank rejection at settlement without asimulate/settledcall.94009004leaves the payout in progress with funds reserved. A timeout does not prove that the bank rejected the transfer. Callsimulate/settledwithoutcome: "completed"and autr, or withoutcome: "failed", to supply the bank’s final answer.
Failure paths
Fiat payouts can also be failed withPOST /v2/sandbox/transactions/:id/simulate/terminal with { "outcome": "failed" } after payout acceptance, once the document-review gate is clear. While that gate is open the call returns 422 SANDBOX_TRANSACTION_NOT_FORCE_TERMINAL_READY. A payout in an in-progress phase with no transfer reference the simulation can drive yet returns the same 422 for either outcome; retry the call, and read the transaction if it keeps refusing. The same endpoint with { "outcome": "completed", "utr": "..." } requires a settlement-ready payout; calling it before the API has selected a settlement route returns 422 SANDBOX_TRANSACTION_NOT_FORCE_TERMINAL_READY. Other primary sandbox failure paths:
Compliance reject
Send to a destination address whose last 8 characters match one of the compliance-reject suffixes (5A4D4EE5, 5A4D4E5A). The compliance mock resolves the address to a rejected decision, and the payout is held for compliance review — it keeps status: "pending", not an automatic failure. Resolve it with POST /v2/sandbox/transactions/:id/simulate/compliance-decision: { "outcome": "reject" } terminates it as failed with compliance_review_rejected (no on-chain broadcast). Once compliance has rejected the payout, { "outcome": "approve" } is not available — a rejected case can only be terminalized via reject. Full catalog: Withdrawal failure paths.
Document-review reject
A payout the documentation policy requires a document for parks for document review. CallPOST /v2/sandbox/payouts/:id/simulate-review-reject (no body) to reject it:
200 OK returning the payout at its current state. The payout terminates as status: "failed" with failureCode: compliance_rejected, transaction.rejected fires (not transaction.failed), and the reserved funds return to the source balance. Returns 404 PAYOUT_NOT_FOUND if the payout is not awaiting document review. To re-attempt, upload an acceptable document and submit a new payout with a fresh idempotency-key.
Travel Rule paths
Use a VASP-attributed destination (suffix5A50AB1E) to route the payout through the Travel Rule flow. Drive the counterparty leg either with auto-pilot suffixes (AC6BC0DE, ACCEEDED, BAD6A1A4, DEC11A1D) or pre-empt the 10-second timer by calling POST /v2/sandbox/payouts/:id/simulate/counterparty-webhook with { "outcome": "acknowledged" | "approved" | "rejected" | "declined" }. rejected and declined always terminate the payout as status: "failed" with failureCode: travel_rule_rejected — sandbox handles the signal regardless of whether the broadcast has happened (deterministic outcome the dashboard can render). Full catalog (including pre-broadcast Travel Rule failures): Travel Rule scenarios.
Broadcast finality fail
A crypto payout’s finality comes from the chain.POST /v2/sandbox/payouts/:id/simulate/confirm returns 422 SANDBOX_TRANSACTION_NOT_FORCE_TERMINAL_READY for it.
Cancelling a payout
Sandbox uses the production cancel contract — seePOST /v2/payouts/:id/cancel for state-based rules and error codes.
Two sandbox-specific deltas:
- The reliably scriptable cancel window is the non-custodial cosign gate; fiat payouts hand off inline (same as production), so their cancel window collapses to a narrow pre-handoff race that you usually can’t observe from a test.
- There is no “force-cancel” simulate endpoint. To drive
failedon fiat in sandbox, use the failure-suffix protocol (see Failure paths) orPOST /v2/sandbox/transactions/:id/simulate/terminal { outcome: "failed" }.
Webhook events
Every webhook your production endpoint would receive also fires in sandbox, with synthesized data. Configure the sandbox endpoint via the dashboard orPOST /v2/webhooks/endpoints exactly like production.
transaction.failed payloads carry a failureCode when the cause is something your integration can act on — for example user_signature_timeout, user_signature_declined, compliance_review_rejected, insufficient_funds_at_settle, rail_policy_rejected, rail_unavailable, travel_rule_rejected. See the Webhooks reference for full payload schemas.
See the failureMessage symmetry contract on the webhooks reference for the exact rules across the DB row, polled GET, and webhook payload.
Address-suffix matching rules
- EVM (Ethereum / Base / Polygon): last 8 hex characters of the address, case-insensitive.
0x...DEADBEEFmatches suffixDEADBEEF. - Tron: last 8 Base58 characters, case-sensitive.
- Solana: last 8 Base58 characters, case-sensitive.
- Fiat (bank account): last 8 digits of
recipient.accountNumber, all non-digit characters stripped before matching.
APPROVED, SELF_HOSTED Travel Rule resolution, proceed to mocked broadcast).
Consolidated withdrawal address-suffix catalog
Every suffix that is meaningful on a withdrawal destination, sourced fromscenario-suffixes.ts. Suffixes match the last 8 characters of the destination address (lowercased hex for EVM; Base58 verbatim for Tron and Solana).
For scenario-specific detail, see the consolidated withdrawal address-suffix catalog, the Wallet screening catalog, and the Counterparty-outcome catalog. The full programmatic list is the Scenario suffix table.
Stellar destinations that cannot receive
Stellar addresses carry a built-in checksum, so a scenario cannot be encoded in the last 8 characters the way it is on the other chains. Send to one of these published addresses instead. Each is rejected synchronously byPOST /v2/payouts and by order creation, before any balance is reserved:
These are the same four rejections a production Stellar payout meets when the recipient has not opted in to the asset, so they are worth handling before you go live.