Hosts and auth
| Sandbox host | https://api.sandbox.conduit.financial |
| Production host | https://api.conduit.financial |
| Auth header | x-api-key: ck_sandbox_... or ck_live_... |
| Idempotency header | idempotency-key: <uuid> (required where the API reference marks it; replays for 30 days) |
Address format
- EVM: case-insensitive — all-lowercase or a correct EIP-55 checksum both match.
- Solana: Base58, case-sensitive — the address must end in the suffix exactly as written.
Scenario suffixes
| Suffix | Chain | Forces | Observable on |
|---|---|---|---|
12517C00 | EVM | Wallet screening returns elevated risk score (below rejection threshold) | Withdrawal proceeds; status: completed (elevated-risk classification recorded for audit) |
5A4070ED | EVM | Wallet screening matches sanctions list | Withdrawal holds for a compliance decision; status: pending. Same reject-only exit as 5A4D4EE5. |
5A4D4E51 | EVM, Solana | compliance check returns elevated risk (routes as approved at medium threshold) | Withdrawal proceeds; status: completed |
5A4D4E5A | EVM, Solana | compliance check flags sanctions match | Withdrawal holds for a compliance decision; status: pending. Same reject-only exit as 5A4D4EE5. |
5A4D4E9E | EVM, Solana | compliance check passes but holds for manual review | Withdrawal pauses for compliance review; status: pending. Resolve via POST /v2/sandbox/transactions/:id/simulate/compliance-decision (approve → proceeds; reject → rejected). |
5A4D4EAA | EVM, Solana | compliance check passes (approved) | Withdrawal proceeds; status: completed |
5A4D4EE5 | EVM, Solana | compliance check fails (high-risk rejection) | Withdrawal holds for a compliance decision; status: pending. Reject it via POST /v2/sandbox/transactions/:id/simulate/compliance-decision to fail it (failureCode: compliance_review_rejected); approve is not available on an already-rejected review. |
5A4ED0DD | EVM | Travel Rule record created in a non-sendable state | Withdrawal fails pre-broadcast; status: failed, failureCode: travel_rule_rejected |
5A50AB1E | EVM | Wallet screening identifies destination as a known exchange | Travel Rule flow triggered; withdrawal proceeds if TR passes |
5E1F0577 | EVM | Wallet screening identifies destination as self-hosted | Travel Rule skipped; withdrawal proceeds normally |
94000000 | Fiat | Fiat withdrawal completes successfully | Withdrawal completed; status: completed |
94009001 | Fiat | Rail policy rejects (amount limit, frequency cap, or recipient restriction) | Withdrawal fails; status: failed, failureCode: rail_policy_rejected |
94009002 | Fiat | Insufficient funds at settlement (funds available at reservation) | Withdrawal fails; status: failed, failureCode: insufficient_funds_at_settle |
94009003 | Fiat | No viable rail available for the corridor | Withdrawal fails; status: failed, failureCode: rail_unavailable |
94009004 | Fiat | Bank timeout; settlement remains unknown | Withdrawal stays in progress with funds reserved; resolve through simulate/settled |
95000000 | Fiat | compliance check passes for inbound fiat deposit | Deposit credited; status: completed |
95009001 | Fiat | compliance check fails for inbound fiat deposit | Deposit holds for a compliance decision; status: pending. Reject it via POST /v2/sandbox/transactions/:id/simulate/compliance-decision to freeze (failureCode: compliance_hold); approve is not available on an already-rejected review. |
95009002 | Fiat | compliance check flags sanctions match on inbound fiat deposit | Deposit holds for a compliance decision; status: pending. Same reject-only exit as 95009001. |
95009003 | Fiat | compliance check returns elevated risk on inbound fiat deposit (routes as approved) | Deposit credited; status: completed |
AC6BC0DE | EVM | Receiving institution acknowledges transfer (informational only) | Withdrawal proceeds; status: completed |
ACCEEDED | EVM, Solana | Receiving institution accepts transfer (informational under FATF Rec. 16) | Withdrawal proceeds; status: completed |
BAD6A1A4 | EVM, Solana | Receiving institution rejects transfer | Pre-broadcast: status: failed, failureCode: travel_rule_rejected. Post-broadcast: withdrawal completes (on-chain transfer cannot be reversed). |
BAD7E517 | EVM, Solana | Travel Rule pre-send validation rejects the transfer | Withdrawal fails pre-broadcast; status: failed, failureCode: travel_rule_rejected |
D15CCAD0 | EVM | Wallet attestation conflict: customer attests the destination is their own wallet but screening identifies a VASP | Withdrawal fails; status: failed, failureCode: travel_rule_rejected |
DA171465 | EVM, Solana | Receiving institution requests additional information; auto-resolves to accepted | Withdrawal proceeds; Travel Rule row transitions to accepted post-broadcast. status: completed |
DE5E11F1 | EVM, Solana | Deposit parked at sender-info gate; it stays held after the deadline | Deposit paused; customer clears via POST /v2/transactions/:id/sender-information with register: true |
DEAA49F1 | EVM, Solana | compliance check passes for inbound crypto deposit | Deposit credited; status: completed |
DEAA5A4D | EVM, Solana | compliance check flags sanctions match on inbound crypto deposit | Deposit holds for a compliance decision; status: pending. Same reject-only exit as DEAA8E51. |
DEAA8157 | EVM, Solana | compliance check returns elevated risk on inbound crypto deposit (routes as approved) | Deposit credited; status: completed |
DEAA8E51 | EVM, Solana | compliance check fails for inbound crypto deposit | Deposit holds for a compliance decision; status: pending. Reject it via POST /v2/sandbox/transactions/:id/simulate/compliance-decision to freeze (failureCode: compliance_hold); approve is not available on an already-rejected review. |
DEAA9E9E | EVM, Solana | compliance check passes but holds for manual review on inbound crypto deposit | Deposit pauses for compliance review; status: pending. Resolve via POST /v2/sandbox/transactions/:id/simulate/compliance-decision (approve → credited; reject → frozen). |
DEC11A1D | EVM, Solana | Receiving institution declines transfer | Pre-broadcast: status: failed, failureCode: travel_rule_rejected. Post-broadcast: withdrawal completes. |
Simulate endpoints
Crypto signs for real here. No signing simulator applies. Sign with a real passkey, or a machine stamp at
POST /v2/signing-requests/:id/approve — see Use a real testnet and Machine-signer stamping.| Endpoint | Drives | Body | Response | Errors |
|---|---|---|---|---|
POST /v2/sandbox/applications/:id/simulate/decision | Application -> APPROVED or REJECTED | { outcome: "approved" | "rejected", category?, field?, reason? } | 200 | APPLICATION_NOT_FOUND, REJECTION_CATEGORY_NOT_APPLICABLE |
POST /v2/sandbox/customers/:customerId/virtual-accounts/:virtualAccountId/deposits/simulate | Accepts a new fiat deposit for ingestion; outcome steers compliance branch. Returns ; transaction.created fires but does not carry it — correlate with GET /v2/transactions?externalReference=. | { outcome: "completed" | "frozen" | "returned" (default completed), assetAmount, senderInfo?, detectedAt?, externalReference?, reason? } | 202 | VALIDATION_ERROR, NOT_FOUND |
POST /v2/sandbox/transactions/:id/simulate/terminal | Transaction -> COMPLETED or FAILED | { outcome: "completed" | "failed", utr?, reason? } | 200 | SANDBOX_TRANSACTION_NOT_FORCE_TERMINAL_READY, CONFLICT, NOT_FOUND |
POST /v2/sandbox/transactions/:id/simulate/compliance-decision | Transaction parked for compliance review -> resumes (approve) or terminalizes at the type default (reject: deposit -> frozen, withdrawal -> rejected). Approve returns 409 once the review is already rejected (a rejected case can only be terminalized via reject). It can also answer 409 once a Conduit analyst sent a request for information on the transaction; Conduit then completes the review. | { outcome: "approve" | "reject" } | 202 | TRANSACTION_NOT_FOUND, VALIDATION_ERROR, CONFLICT |
POST /v2/sandbox/wallets/registered-addresses/:id/simulate/compliance-decision | Registered address parked pending_screening (0x999 review magic value) -> registered + deposits from the address that were awaiting registration resume (approve) or suspended as sanctions-blocked (reject); same decision replays 200, a conflicting one is 409 | { outcome: "approve" | "reject" } | 200 | REGISTERED_ADDRESS_NOT_FOUND, REGISTERED_ADDRESS_INVALID_TRANSITION, VALIDATION_ERROR |
POST /v2/sandbox/payouts/:id/simulate/settled | Fiat payout -> rail settlement | { outcome: "completed" | "failed", utr?, reason? } | 200 | PAYOUT_NOT_FOUND, SANDBOX_TRANSACTION_NOT_FORCE_TERMINAL_READY, CONFLICT |
POST /v2/sandbox/payouts/:id/simulate/counterparty-webhook | Travel-Rule counterparty resolution | { outcome: "acknowledged" | "approved" | "rejected" | "declined", reason? } | 200 | PAYOUT_NOT_FOUND |
POST /v2/sandbox/payouts/:id/simulate-review-approve | Every payout review (document gate, compliance hold, or both) -> released; returns once the payout has left review. 409 when a verdict on the review already stands. It can also answer 409 once a Conduit analyst sent a request for information on the payout; Conduit then completes the review, and simulate-review-reject still ends the payout. 503 when the payout has not left review within the wait: the release stands, so retry | {} | 200 | PAYOUT_NOT_FOUND, CONFLICT, SERVICE_UNAVAILABLE |
POST /v2/sandbox/payouts/:id/simulate-review-reject | Payout review (document gate or compliance hold) -> rejected; funds returned | {} | 200 | PAYOUT_NOT_FOUND, CONFLICT |
POST /v2/sandbox/rfis/:id/simulate/resolve | RFI open or responded -> resolved; rfi.resolved fires. On a payout document RFI, approved releases the payout and rejected fails it | { outcome: "approved" | "rejected", resolutionSummary? } | 200 | RFI_NOT_FOUND, RFI_INVALID_STATUS_TRANSITION |
POST /v2/sandbox/rfis/:id/simulate/more-info | RFI open or responded -> open with a new round; rfi.more_info_requested fires. A reopened RFI shows the same way | { ask, dueAt? (default 7 days from now) } | 200 | RFI_NOT_FOUND, RFI_INVALID_STATUS_TRANSITION |
POST /v2/sandbox/rfis/simulate | Opens an RFI on your customer as Conduit compliance does -> open; rfi.published fires. Answer, resolve or ask again with the RFI routes | { customerId, ask, title?, dueAt? (default 48 hours from now) } | 201 | CUSTOMER_NOT_FOUND, VALIDATION_ERROR |
POST /v2/sandbox/customers/:customerId/simulate-reset-claim | Single-shot reset of a customer’s wallet claim. Removes every signer, every wallet, and the crypto-wallet feature flag, so the next claim-non-custodial starts fresh. Refuses with 409 CLAIM_RESET_BLOCKED if any row still references this customer’s wallet setup. A reset cannot delete a record that still references this setup; on a non-custodial account, add or remove a signer on the roster to change who signs, which keeps the wallets and their deposit addresses. A custodial account that still holds a balance refuses with 409 CONFLICT: create a new customer instead. | {} | 200 | PROVIDER_ACCOUNT_NOT_FOUND, CLAIM_RESET_BLOCKED, CONFLICT |
POST /v2/sandbox/customers/:customerId/simulate/restrict | Customer -> restricted as Conduit compliance restricts it. full_freeze and withdrawals_suspended fire customer.restricted; a blocked money movement returns 403 CAPABILITY_SUSPENDED | { restrictionType: "full_freeze" | "withdrawals_suspended" | "conversions_disabled" } | 200 | CUSTOMER_NOT_FOUND |
POST /v2/sandbox/customers/:customerId/simulate/lift-restriction | Customer restriction of that type -> lifted; the blocked money movements are allowed again | { restrictionType: "full_freeze" | "withdrawals_suspended" | "conversions_disabled" } | 200 | CUSTOMER_NOT_FOUND |
PATCH /v2/sandbox/customers/:customerId/house-account | Marks (or unmarks) one of your business customers as a house account — the only source a purpose: prefunding payout accepts. Every sandbox organization is seeded with one house account already; use this to prefund from a customer you onboarded yourself. On live, only Conduit can set the marker. | { isHouseAccount: boolean } | 200 | CUSTOMER_NOT_FOUND, HOUSE_ACCOUNT_BUSINESS_ONLY |
POST /v2/sandbox/orders/:id/simulate/rate-lock-expired | Order -> rate-lock-expired cancellation (within seconds; wait for order.cancelled) | {} | 200 | NOT_FOUND |
POST /v2/sandbox/orders/:id/simulate/conversion-failed | Order -> conversion-failed terminal | { reason? } | 200 | NOT_FOUND |
POST /v2/sandbox/orders/:orderId/deposits/simulate/compliance-decision | The compliance review holding a transfer into the order’s funding address -> resolved. approve releases it to fund the order; reject holds the funds permanently and the order goes unfunded | { outcome: "approve" | "reject" } | 202 | ORDER_NOT_FOUND, SANDBOX_ORDER_NOT_DEPOSIT_FUNDED, SANDBOX_ORDER_NO_PARKED_FUNDING, CONFLICT |
POST /v2/sandbox/whitelist-recipients/:id/simulate-approve | Whitelist recipient PENDING_REVIEW -> REGISTERED | {} | 200 | WHITELIST_RECIPIENT_NOT_FOUND, WHITELIST_INVALID_TRANSITION |
POST /v2/sandbox/whitelist-recipients/:id/simulate-reject | Whitelist recipient PENDING_REVIEW -> REJECTED | {} | 200 | WHITELIST_RECIPIENT_NOT_FOUND, WHITELIST_INVALID_TRANSITION |
Index by entity
| If you have a… | You can call… |
|---|---|
app_... (application) | POST /v2/sandbox/applications/:id/simulate/decision |
cus_... (customer) | PATCH /v2/sandbox/customers/:customerId/house-account (mark it as a prefunding source), POST /v2/sandbox/customers/:customerId/simulate-reset-claim, .../simulate/restrict, .../simulate/lift-restriction, POST /v2/sandbox/rfis/simulate (open an RFI on it) |
vac_... (virtual account) | POST /v2/sandbox/customers/:customerId/virtual-accounts/:virtualAccountId/deposits/simulate |
wra_... (registered address) | POST /v2/sandbox/wallets/registered-addresses/:id/simulate/compliance-decision (when parked pending_screening via the 0x999 review magic value) |
txn_... (transaction, deposit or payout) | POST /v2/transactions/:id/sender-information with register: true to clear a parked deposit (not sandbox-only — the production endpoint); POST /v2/sandbox/transactions/:id/simulate/terminal; payouts also: .../payouts/:id/simulate/settled, .../simulate/counterparty-webhook, .../simulate-review-approve, .../simulate-review-reject |
rfi_... (request for information) | POST /v2/sandbox/rfis/:id/simulate/resolve, .../simulate/more-info |
ord_... (order) | POST /v2/sandbox/orders/:id/simulate/rate-lock-expired, .../simulate/conversion-failed, .../deposits/simulate/compliance-decision (deposit-funded orders — an order created with no source) |
Index by lifecycle state
Crypto signs and settles for real. At
stage: "awaiting_signature", approve with a real passkey or a machine stamp at POST /v2/signing-requests/:id/approve; at stage: "settling", wait for real on-chain finality (minutes) — there is no override. See Use a real testnet.| Payout state | What you can call |
|---|---|
stage: "awaiting_signature" | Approve for real (passkey, or POST /v2/signing-requests/:id/approve). simulate/counterparty-webhook drives a Travel Rule pre-broadcast reject. |
| parked at a review (a document the policy requires, or a compliance hold) | simulate-review-approve to release the payout, or simulate-review-reject to terminate it as failed (reserved funds returned). One pair of levers resolves the payout’s reviews, because a payout never reports which reviews hold it; one approve call releases all of them. transaction.under_review is the event that tells you a review opened. |
stage: "settling" | Wait for real on-chain finality (minutes) — no override. |
completed / failed | nothing; the review levers and simulate/terminal return 200 with the payout at the state it reached, while simulate/settled returns 409 CONFLICT. |
| Deposit state | What you can call |
|---|---|
| (parked at sender-info gate) | POST /v2/transactions/:id/sender-information with register: true |
terminal (completed / frozen / returned) | nothing; outcome was set at ingestion time |
| Order state | What you can call |
|---|---|
pending (rate-lock window open) | simulate/rate-lock-expired (cancels within seconds) |
| any pre-terminal state with a conversion leg | simulate/conversion-failed |
| any state, deposit-funded orders only | Send real testnet funds from a registered address to the order’s funding address; see Deposit-Funded Orders. |
| deposit-funded orders, transfer held for review | deposits/simulate/compliance-decision {outcome: "approve" | "reject"} — the order identifies which funding transfer to resolve. 404 SANDBOX_ORDER_NO_PARKED_FUNDING when nothing is held. |
| RFI state | What you can call |
|---|---|
open or responded | simulate/resolve {outcome: "approved" | "rejected"} to resolve it (rfi.resolved fires), or simulate/more-info {ask} to open a new round (rfi.more_info_requested fires). A reopened RFI shows the same way as more-info: status open, a new round, and rfi.more_info_requested. |
draft, resolved or cancelled | nothing; both levers answer 409 RFI_INVALID_STATUS_TRANSITION |
| Application state | What you can call |
|---|---|
pending | simulate/decision {outcome: "approved" | "rejected"} |
cancelled | simulate/decision {outcome: "approved" | "rejected"} — an undecided application cancels after 14 days; simulate/decision still decides it, including a cancelled one |
processing, individual whose attestation matches and shows a verification date, with the ID document attached | Nothing: it auto-approves, usually within a few minutes. See Customer KYC. |
approved or rejected | simulate/decision with the same outcome answers 200 with the application unchanged; the other outcome answers 409 APPLICATION_ALREADY_DECIDED |
Index by error code
| If you got… | The call that produced it… |
|---|---|
CONFLICT (409) | simulate/settled against a payout that already reached its final state. Read the payout for its final state. The review levers and simulate/terminal instead return 200 with the payout at the state it reached. simulate-reset-claim on a custodial account that still holds a balance also returns it: create a new customer. |
SANDBOX_TRANSACTION_NOT_FORCE_TERMINAL_READY (422) | simulate/terminal or simulate/settled called before the gate its outcome depends on has cleared. See playbook. |
APPLICATION_NOT_FOUND (404) | simulate/decision when the application ID does not exist or does not belong to the org. |
REJECTION_CATEGORY_NOT_APPLICABLE (422) | simulate/decision with a category field on a non-KYB application. |
Public failure codes
| Code | Terminal state | Channel | Description | Playbook |
|---|---|---|---|---|
chain_broadcast_failed | failed | webhook + polled | The signed payout could not be broadcast to the chain before reaching finality; no funds left the wallet | Playbook |
compliance_hold | failed | webhook + polled | Compliance review held the funds; manual remediation required | Playbook |
compliance_rejected | failed | webhook + polled | A compliance review rejected the payout; reserved funds were returned (a document rejection fires transaction.rejected, not transaction.failed) | Playbook |
compliance_review_rejected | failed | webhook + polled | Transaction rejected in regulatory compliance review; non-retryable | Playbook |
crypto_wallet_misconfigured | failed | webhook + polled | The source wallet is missing required signing configuration; the payout could not be signed | Playbook |
insufficient_funds | failed | webhook + polled | Source balance was insufficient to reserve the payout before broadcast; money in frozen, including money a bank block holds, does not count; no funds moved | Playbook |
insufficient_funds_at_settle | failed | webhook + polled | Source funds were insufficient at the settlement attempt | Playbook |
insufficient_onchain_balance | failed | webhook + polled | The source wallet has insufficient real on-chain balance for the send, so the transfer could not be broadcast; no funds moved. See Use a real testnet to fund the wallet, then retry | Playbook |
provider_rejected | failed | webhook + polled | The broadcast request was declined before the transaction was submitted to the chain | Playbook |
rail_policy_rejected | failed | webhook + polled | The receiving rail rejected the payment per its policy | Playbook |
rail_unavailable | failed | webhook + polled | The chosen rail was temporarily unavailable | Playbook |
returned_by_sender | failed | webhook + polled | A bank returned the transfer: an inbound transfer before credit, or a completed payout the recipient’s bank sent back | Playbook |
roster_changed | failed | webhook + polled | A signer on the customer’s roster was removed (or demoted out of the signing pool) while the payout was awaiting signatures; the half-collected stamps were voided so you can re-initiate | Playbook |
sender_info_timeout | failed | webhook + polled | Legacy: the sender-information deadline passed on an older deposit. Not set on new transactions | Playbook |
source_account_unavailable | failed | webhook + polled | The source balance is held on a bank account that cannot move funds now, because the account is not active (for example closed, or still in approval); no funds moved. Money on a bank account the bank has blocked is reported in frozen and fails as insufficient_funds instead. Contact support to make the balance available on an active account, then submit a new order | Playbook |
travel_rule_rejected | failed | webhook + polled | Counterparty rejected the Travel Rule request, or Travel Rule validation failed pre-broadcast | Playbook |
user_signature_declined | failed | webhook + polled | End user explicitly declined to sign | Playbook |
user_signature_expired | failed | webhook + polled | End user did not sign across the allowed signing windows; the request expired | Playbook |
user_signature_rejected_by_provider | failed | webhook + polled | Signing provider rejected the signature payload | Playbook |
user_signature_timeout | failed | webhook + polled | Payout timed out waiting in the wallet’s signing queue before it could start collecting signatures | Playbook |
Sandbox simulate endpoint errors
These codes are returned as HTTP errors by sandbox simulate endpoints. They are notfailureCode values on transactions.
| Code | HTTP status | Description | Playbook |
|---|---|---|---|
SANDBOX_TRANSACTION_NOT_FORCE_TERMINAL_READY | 422 | Called a sandbox simulate endpoint whose requested outcome is not viable at the transaction’s current phase yet | Playbook |
HTTP status code legend
| Code | Meaning |
|---|---|
| 200 | Mutation succeeded; resource returned |
| 202 | Accepted; work is in flight — see the route’s row for what comes back |
| 400 | Validation error; check the pointer in the response |
| 401 | Missing or invalid API key |
| 403 | API key lacks permission |
| 404 | Resource not found |
| 409 | Conflict; inspect type for the precise error |
| 429 | Rate limited |
| 500 | Server error |
Webhook event topics (most common)
application.approved/application.rejectedtransaction.created/transaction.completed/transaction.failed/transaction.cancelledtransaction.awaiting_signature/transaction.awaiting_sender_informationorder.succeeded/order.failedcustomer.createdclaim.completedvirtual_account.activated