The sandbox cluster is fully mocked. There is no real chain, no real banking connection, no third-party vendor calls. Every outcome is deterministic and driven by your request data — address suffixes for crypto deposits, account-number suffixes for fiat deposits, or an explicit outcome field on the deposit simulate body — or by sandbox-only simulate/* endpoints. See Sandbox overview for the full posture.
This page covers two deposit types:
- Fiat deposits land on a customer’s virtual account via a simulate endpoint. The balance is credited immediately; no bank transfer occurs.
- Crypto deposits land on a customer’s wallet via a simulate endpoint. The deposit enters the same ingestion pipeline as a real provider webhook, including compliance screening and the sender-information gate.
Use fiat deposits when testing order-funded flows (onramps, conversions). Use crypto deposits when testing the inbound wallet flow including compliance and Travel Rule sender-info scenarios.
Prerequisites
- A sandbox API key for an
ACTIVE customer. Set SANDBOX_API_KEY, CUSTOMER_ID, VA_ID (virtual account), and WALLET_ID in your shell.
- For fiat: the virtual account must be
ACTIVE for asset USD.
- For crypto: the wallet must be
ACTIVE and have a deposit address.
- Base URL:
https://api.sandbox.conduit.financial.
The idempotency-key header is required on every money-moving POST. The cache TTL is 300 seconds — replay the same key within that window to safely retry; use a fresh key for a new operation.
Fiat deposit — happy path
Inject a synthetic USD deposit into a customer’s virtual account.
POST https://api.sandbox.conduit.financial/v2/sandbox/customers/{customerId}/virtual-accounts/{virtualAccountId}/deposits/simulate
Request body:
| Field | Type | Required | Description |
|---|
assetAmount | object | yes | { "code": "USD", "amount": "1000.00" }. amount is a canonical decimal string at USD precision (2 decimals). |
outcome | enum | no | "completed" (default), "frozen", or "returned". frozen parks the deposit in a sanctions-freeze terminal state. returned parks it in an AML-return terminal state. Equivalent to the senderInfo.accountNumber suffix catalog but explicit; takes precedence when both are set. |
rail | string | no | "ACH", "FEDWIRE", or "RTP". Defaults to ACH |
senderInfo | object | no | Synthetic sender details (name, accountNumber, routingNumber, iban, bic, country). Pass senderInfo.accountNumber ending in a fiat AML suffix to force a compliance outcome (see Suffix catalog). |
externalReference | string | no | Custom reference for the synthetic transfer |
curl -X POST "https://api.sandbox.conduit.financial/v2/sandbox/customers/${CUSTOMER_ID}/virtual-accounts/${VA_ID}/deposits/simulate" \
-H "x-api-key: ${SANDBOX_API_KEY}" \
-H "idempotency-key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"assetAmount": { "code": "USD", "amount": "1000.00" }
}'
201 Created with { "status": "received", "inboxEntryId": "..." }. The deposit enters the ingestion pipeline and the customer’s USD balance updates within a few seconds. Your webhook endpoint receives transaction.completed.
Fiat deposit — compliance failure paths
Fiat deposits use the same suffix protocol as crypto deposits, matched against the last 8 digits of the sender’s bank account number (non-digit characters stripped). Pass a senderInfo.accountNumber ending in a documented suffix to force a specific AML outcome.
The deposit-specific suffix values are listed in the Suffix catalog below. For the consolidated suffix table across deposits and withdrawals, see Scenario suffixes.
Crypto deposit — happy path
Inject a synthetic on-chain deposit into a customer’s wallet.
POST https://api.sandbox.conduit.financial/v2/sandbox/customers/{customerId}/wallets/{walletId}/deposits/simulate
Request body:
| Field | Type | Required | Description |
|---|
assetAmount | object | yes | { "code": "USDC", "chain": "ETHEREUM", "amount": "100" }. amount is a canonical decimal string at the asset’s precision (USDC has 6 decimals). |
outcome | enum | no | "completed" (default), "frozen", or "returned". frozen parks the deposit in a sanctions-freeze terminal state. returned parks it in an AML-return terminal state. Equivalent to the sourceAddress suffix catalog but explicit; takes precedence when both are set. |
sourceAddress | string | no | Sender address. Omit to get a synthetic deterministic address |
txHash | string | no | Synthetic transaction hash. Omit to get a deterministic synthetic hash (see Note below). Pass an explicit random value to bust the dedupe key when re-firing the same body. |
originator | object | no | Originator details (entityType, legalName, country). Defaults to a generic sandbox sender on the happy path. The default is automatically suppressed when the sender-information drivers fire (sourceAddress ending in DE5E11F0, or amount at/above the 10,000-unit Travel Rule threshold) so the gate parks instead of clearing. Pass an explicit originator object to override and clear the gate. Pass null to suppress the default originator on the happy path (no other driver firing) — note that this alone does not force the gate when the source address is already registered. |
Synthetic txHash is deterministic. When you omit txHash, the sandbox derives one from a hash of (organizationId, customerId, walletId, chain, assetCode, amount, externalReference). Two identical request bodies produce the same txHash, and the second call is dedupe-rejected with { "status": "duplicate" } instead of a fresh deposit row. Pass an explicit random txHash per request to bust dedupe, for example TXHASH="0x$(openssl rand -hex 32)" then include "txHash": "$TXHASH" in the body.Dedupe also varies by finality state and sender. Two probes of the same tx at different finality states (e.g. submitted then finalized) ingest as separate rows. On the fiat side, two same-amount deposits with different senderInfo.accountNumber (e.g. swapping AML suffixes) both ingest cleanly — no need to vary the amount.
curl -X POST "https://api.sandbox.conduit.financial/v2/sandbox/customers/${CUSTOMER_ID}/wallets/${WALLET_ID}/deposits/simulate" \
-H "x-api-key: ${SANDBOX_API_KEY}" \
-H "idempotency-key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"assetAmount": { "code": "USDC", "chain": "ETHEREUM", "amount": "100" }
}'
201 Created with { "status": "received", "inboxEntryId": "..." }. The deposit enters the ingestion pipeline: compliance screens the source address (default sandbox originator clears automatically), and the balance updates within a few seconds. Your webhook endpoint receives transaction.completed.
To force a specific compliance outcome, pass a sourceAddress whose last 8 hex characters match one of the suffixes in the Suffix catalog below.
The sender-information gate fires when the receiving VASP needs originator details to satisfy Travel Rule. In sandbox there are two ways to drive it deterministically — pick whichever is easier for the test you’re writing:
Driver 1 — source-address suffix. Pass a sourceAddress ending in DE5E11F0. The gate fires regardless of the amount or whether the address is pre-registered.
curl -X POST "https://api.sandbox.conduit.financial/v2/sandbox/customers/${CUSTOMER_ID}/wallets/${WALLET_ID}/deposits/simulate" \
-H "x-api-key: ${SANDBOX_API_KEY}" \
-H "idempotency-key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"assetAmount": { "code": "USDC", "chain": "ETHEREUM", "amount": "100" },
"sourceAddress": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaade5e11f0"
}'
Driver 2 — Travel Rule amount threshold. Send a crypto deposit at or above 10,000 in canonical asset units (e.g. "10000" USDC, which is 10_000.000000 at full precision). The gate fires regardless of address or suffix, matching the FATF R.16 / FinCEN funds-transmittal posture where high-value transfers always require originator information.
curl -X POST "https://api.sandbox.conduit.financial/v2/sandbox/customers/${CUSTOMER_ID}/wallets/${WALLET_ID}/deposits/simulate" \
-H "x-api-key: ${SANDBOX_API_KEY}" \
-H "idempotency-key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"assetAmount": { "code": "USDC", "chain": "ETHEREUM", "amount": "10000" }
}'
The deposit parks at status: pending and your webhook endpoint receives transaction.awaiting_sender_information with a daysRemaining count and a deadlineAt timestamp.
Re-emission cadence. transaction.awaiting_sender_information fires once on initial park, then re-emits as the deadline approaches with the updated daysRemaining. Terminal failure emits daysRemaining: null and persists failureCode: SENDER_INFO_TIMEOUT on the transaction row.
Sandbox vs. production deadline — what changes and what doesn’t: The daysRemaining and deadlineAt fields in the webhook payload always reflect the real 30-day deadline, even in sandbox. deadlineAt is detectedAt + 30 days and daysRemaining is approximately 30 on the initial fire. Your webhook handler should branch on these values as if they are live-equivalent — they are.
What sandbox compresses is the server-side timeout: instead of waiting 30 days, the timeout fires after ~30 seconds. So the deposit auto-fails fast in a single test run, but the contract fields your code sees stay identical to production. This is the central sandbox promise: live-equivalent payload contract, compressed timeout.
Option A — wait for auto-timeout
Do nothing. After ~30 seconds the sandbox timer fires and the deposit auto-fails with failureCode: SENDER_INFO_TIMEOUT; your webhook endpoint receives transaction.failed.
Polled GET after timeout. The public GET /v2/transactions/:id response surfaces failureCode: SENDER_INFO_TIMEOUT, matching the transaction.failed webhook payload. The polled response and the webhook never drift.
Option B — resolve manually via simulate endpoint
Call the simulate endpoint to provide sender information and clear the gate immediately. Returns 200 OK with the deposit at its current state.
POST https://api.sandbox.conduit.financial/v2/sandbox/customers/{customerId}/deposits/{depositId}/simulate/sender-info
curl -X POST "https://api.sandbox.conduit.financial/v2/sandbox/customers/${CUSTOMER_ID}/deposits/${DEPOSIT_ID}/simulate/sender-info" \
-H "x-api-key: ${SANDBOX_API_KEY}" \
-H "idempotency-key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"originator": {
"entityType": "BUSINESS",
"legalName": "Acme Corp",
"country": "USA"
}
}'
200 OK returning the deposit at its current state. The gate clears, the auto-timeout timer is cancelled, and the deposit proceeds to its next phase.
Force a returned deposit
Both the fiat and crypto deposit simulate endpoints accept an explicit outcome field that pre-decides the compliance branch at ingestion time. The values are:
"completed" (default) - deposit clears compliance and credits the destination balance.
"frozen" - deposit terminates as failed with failureCode: "COMPLIANCE_HOLD". Equivalent to passing a suffix that resolves to a non-CLEAR AML decision, but explicit.
"returned" - deposit terminates as failed with failureCode: "RETURNED_BY_SENDER". Models a fiat-rail return or a crypto reversal from the originating institution before credit. There is no suffix that triggers this branch.
curl -X POST "https://api.sandbox.conduit.financial/v2/sandbox/customers/${CUSTOMER_ID}/virtual-accounts/${VA_ID}/deposits/simulate" \
-H "x-api-key: ${SANDBOX_API_KEY}" \
-H "idempotency-key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"outcome": "returned",
"assetAmount": { "code": "USD", "amount": "1000.00" }
}'
201 Created returns the public deposit transaction view (the same shape as GET /v2/transactions/:id). The deposit then terminates failed and your webhook endpoint receives transaction.failed with failureCode: "RETURNED_BY_SENDER". The same outcome field is accepted on the crypto deposit simulate endpoint with identical semantics.
When outcome is set, it takes precedence over the suffix-driven branch. Pass outcome: "completed" (or omit the field) to keep the suffix protocol in effect.
Force a parked deposit terminal
After a deposit transaction exists, POST https://api.sandbox.conduit.financial/v2/sandbox/transactions/{depositId}/simulate/terminal can force the deposit workflow to a terminal outcome. Use this when you need to pre-empt a compliance park, sender-information wait, or slow async path after locating the deposit id through GET /v2/transactions?type=DEPOSIT. Body is { "outcome": "completed" | "failed", "utr"?: "...", "reason"?: "..." }. outcome: "completed" posts the incoming deposit first when needed and then approves it; outcome: "failed" compensates an already-posted incoming deposit before writing the failed terminal state. Returns the deposit at its current state.
Address format. EVM addresses must be all-lowercase OR a correctly EIP-55 checksummed mixed-case form. The mnemonic suffixes called out in this page are uppercase for readability; the wire-format addresses you send to the API are all-lowercase.
Suffix catalog
Suffixes are matched against the last 8 characters of the source identifier:
- Crypto deposits: last 8 hex characters of
sourceAddress (case-insensitive on EVM; Base58 verbatim on Tron and Solana).
- Fiat deposits: last 8 digits of
senderInfo.accountNumber (non-digit characters stripped before matching).
Addresses and account numbers not matching any suffix take the happy path: compliance approved, deposit completes.
Crypto deposit suffixes (matched on sourceAddress)
| Suffix | Scenario | failureCode |
|---|
DEAA4900 | AML approved (explicit happy path) | — |
DEAA8E50 / DEAA5A4D | AML non-CLEAR (high-risk or sanctions) — deposit frozen † | COMPLIANCE_HOLD |
DEAA8157 | AML risky — no observable difference on the public surface; the deposit completes as APPROVED. The dashboard records the risky classification for audit. | — |
DE5E11F0 | Sender-information gate required — deposit parks; sandbox timer fires after ~30 s | SENDER_INFO_TIMEOUT (if not resolved) |
Fiat deposit suffixes (matched on senderInfo.accountNumber digits)
| Suffix | Scenario | failureCode |
|---|
95000000 | AML approved (explicit happy path) | — |
95009001 / 95009002 | AML non-CLEAR (high-risk or sanctions) — deposit frozen † | COMPLIANCE_HOLD |
95009003 | AML risky — routes approved at current threshold | — |
† On deposits, every non-CLEAR AML classification (high-risk or sanctions match) routes to a frozen terminal — the deposit cannot be credited until compliance review. The public failure code is COMPLIANCE_HOLD in all cases; the underlying classification is recorded on the dashboard for audit. If the fiat deposit is the source funding event for the oldest pending autoExecute: true ONRAMP order that matches the deposit tuple and its amount could cover that order’s total debit, that order also emits order.failed with reasonCode: "PROVIDER_REJECTED".
The DEAA8157 suffix (crypto) and 95009003 suffix (fiat) trigger an elevated-risk AML classification that is recorded internally for audit. At current thresholds this classification routes APPROVED on the public surface, so there is no observable difference from a clean deposit. Use these suffixes to exercise audit-trail emission only; do not branch your integration logic on them.
Webhook events
| Event | When it fires |
|---|
transaction.created | Immediately after the simulate call is accepted |
transaction.awaiting_sender_information | Deposit parks at the sender-information gate. Two drivers: sourceAddress ending in DE5E11F0 (suffix-keyed, fires regardless of pre-registration) or deposit amount at/above the 10,000-unit Travel Rule threshold (amount-keyed, fires regardless of suffix). |
transaction.completed | Deposit reaches terminal completed state |
transaction.failed | Deposit reaches terminal failed state (AML non-CLEAR, sender-info timeout); fiat source deposits can also fail a matching amount-covered pending auto-execute ONRAMP order |
transaction.failed payloads carry a failureCode when the cause is actionable. transaction.awaiting_sender_information includes daysRemaining and deadlineAt — these always reflect the real 30-day deadline (the same values your production handler would see). Only the sandbox internal timer is compressed to ~30 seconds so the timeout path is fast to test.
Errors
See Errors for the full catalog. Deposit-relevant failure codes:
COMPLIANCE_HOLD — compliance screening returned a non-CLEAR decision (high-risk or sanctions match); the deposit is frozen and cannot be credited.
RETURNED_BY_SENDER — the inbound transfer was returned by the originating institution before it could be credited. Sandbox triggers this branch via the outcome: "returned" field on the deposit simulate endpoint.
SENDER_INFO_TIMEOUT — the sender-information gate expired before details were provided; resubmit with originator details included up front.
Diagrams
Crypto deposit state machine
Related pages