> ## Documentation Index
> Fetch the complete documentation index at: https://v2.docs.conduit.financial/llms.txt
> Use this file to discover all available pages before exploring further.

# Deposits in sandbox

> End-to-end guide for testing fiat and crypto deposits: happy paths, compliance failures, and the sender-information gate

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](/sandbox/overview) for the full posture.

This page covers three deposit types, one per fundable resource:

* **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.
* **Deposit-funded order transfers** land on the funding address the order publishes. The endpoint answers with the **order**, not a deposit — but the transfer itself is still an ordinary `deposit` transaction you can read back. Only the section below applies to them — see [Deposit-funded orders](/concepts/deposit-funded-orders) for the concept.

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. Use the order-keyed endpoint when the order published a funding address instead of debiting a wallet you named.

## 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`.

<Note>
  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.
</Note>

## 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 a compliance-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 compliance suffix to force a compliance outcome (see [Suffix catalog](#suffix-catalog)).                                                        |
| `externalReference` | string | no       | Custom reference for the synthetic transfer. **Unique across the entire sandbox, not just your organization** — omit it to get a collision-free synthetic reference (see the warning below)                                                                                                     |

<CodeGroup>
  ```bash curl theme={null}
  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" }
    }'
  ```

  ```typescript typescript theme={null}
  const res = await fetch(
    `${process.env.SANDBOX_HOST}/v2/sandbox/customers/${customerId}/virtual-accounts/${vaId}/deposits/simulate`,
    {
      method: "POST",
      headers: {
        "x-api-key": process.env.SANDBOX_API_KEY!,
        "idempotency-key": crypto.randomUUID(),
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ assetAmount: { code: "USD", amount: "1000.00" } }),
    },
  );
  const { externalReference } = await res.json();
  // 202 acknowledgement: { externalReference: "sandbox_..." } — the reference the
  // deposit is detected under. Resolve the deposit itself with:
  //   GET /v2/transactions?type=deposit&externalReference=${externalReference}
  ```

  ```python python theme={null}
  import httpx, os, uuid

  r = httpx.post(
      f"{os.environ['SANDBOX_HOST']}/v2/sandbox/customers/{customer_id}/virtual-accounts/{va_id}/deposits/simulate",
      headers={
          "x-api-key": os.environ["SANDBOX_API_KEY"],
          "idempotency-key": str(uuid.uuid4()),
          "Content-Type": "application/json",
      },
      json={"assetAmount": {"code": "USD", "amount": "1000.00"}},
  )
  external_reference = r.json()["externalReference"]
  # 202 acknowledgement: {"externalReference": "sandbox_..."} — resolve the deposit with
  #   GET /v2/transactions?type=deposit&externalReference=<external_reference>
  ```
</CodeGroup>

`202 Accepted` means the deposit was handed to the ingestion pipeline. Ingestion is asynchronous — exactly as it is in production, where no endpoint creates a deposit synchronously — so the response body carries only `externalReference`: the reference the deposit is detected under, either the one you sent or a synthetic `sandbox_…` one when you omitted it. Send it back verbatim: surrounding whitespace is trimmed, so the acknowledged reference — not your original string — is the one the deposit is stored under and the one the filter matches. Observe the deposit through the `transaction.created` webhook, or read it back with `GET /v2/transactions?type=deposit&externalReference=…`. Re-sending the same `externalReference` re-acknowledges the existing deposit rather than creating a duplicate. The customer's USD balance updates within a few seconds, and your webhook endpoint receives `transaction.completed`.

<Warning>
  **A reference you choose is unique across the whole sandbox.** The fiat dedup key is the provider plus the reference, with no organization in it, and every sandbox organization shares one mock bank. If another organization already used `invoice-001`, your call is treated as their replay: you get `202`, but no deposit ever appears in *your* transaction list. Omit `externalReference` and let the sandbox derive one — the synthetic value hashes your organization in, so it cannot collide — or prefix yours with something unique to your integration. Crypto deposits are unaffected: their key includes the destination address, which differs per organization.
</Warning>

## 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 compliance outcome.

The deposit-specific suffix values are listed in the [Suffix catalog](#suffix-catalog) below. For the consolidated suffix table across deposits and withdrawals, see [Scenario suffixes](/sandbox/cheat-sheet#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 a compliance-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.                                                                                                                                                                                                                                                                                                                                                                                                                              |
| `externalReference` | string | no       | A salt for the synthetic `txHash` only — vary it to make two otherwise-identical bodies ingest as separate deposits. It is not stored on the deposit and is not a handle you can look the deposit up by: `GET /v2/transactions?externalReference=` matches fiat deposits only. Resolve a crypto deposit with `?chain=&txHash=`, using the `txHash` the `202` returned. Ignored when you pass an explicit `txHash`.                                                                                                                                                                                       |
| `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 `DE5E11F1`, 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. |

<Warning>
  **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 re-acknowledges the existing deposit (idempotent on `(chain, txHash)`) rather than creating a fresh 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 compliance suffixes) both ingest cleanly — no need to vary the amount.
</Warning>

<CodeGroup>
  ```bash curl theme={null}
  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" }
    }'
  ```

  ```typescript typescript theme={null}
  const res = await fetch(
    `${process.env.SANDBOX_HOST}/v2/sandbox/customers/${customerId}/wallets/${walletId}/deposits/simulate`,
    {
      method: "POST",
      headers: {
        "x-api-key": process.env.SANDBOX_API_KEY!,
        "idempotency-key": crypto.randomUUID(),
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        assetAmount: { code: "USDC", chain: "ethereum", amount: "100" },
      }),
    },
  );
  const { chain, txHash } = await res.json();
  // 202 acknowledgement: { chain: "ethereum", txHash: "sandbox_..." } — the identity
  // deposit is detected under. Resolve the deposit itself with:
  //   GET /v2/transactions?type=deposit&txHash=${txHash}
  ```

  ```python python theme={null}
  import httpx, os, uuid

  r = httpx.post(
      f"{os.environ['SANDBOX_HOST']}/v2/sandbox/customers/{customer_id}/wallets/{wallet_id}/deposits/simulate",
      headers={
          "x-api-key": os.environ["SANDBOX_API_KEY"],
          "idempotency-key": str(uuid.uuid4()),
          "Content-Type": "application/json",
      },
      json={
          "assetAmount": {"code": "USDC", "chain": "ethereum", "amount": "100"},
      },
  )
  tx_hash = r.json()["txHash"]
  # 202 acknowledgement: {"chain": "ethereum", "txHash": "sandbox_..."} — resolve with
  #   GET /v2/transactions?type=deposit&txHash=<tx_hash>
  ```
</CodeGroup>

`202 Accepted` means the deposit was handed to the ingestion pipeline. Ingestion is asynchronous — exactly as it is in production, where no endpoint creates a deposit synchronously — so the response body carries only `chain` and `txHash`: the identity the deposit is detected under, either the hash you sent or a synthetic `sandbox_…` one when you omitted it. Poll with the hash the response gave you, not the one you typed — on Ethereum and the other hex-hash chains it comes back lowercased, matching how the deposit is stored. Observe the deposit through the `transaction.created` webhook, or read it back with `GET /v2/transactions?type=deposit&txHash=…`. A hash identifies a transaction, not a row: reuse one across several wallets and the filter returns a deposit per credited wallet, so match `destination.walletId` when you do that. Compliance screens the source address (the default sandbox originator clears automatically), the balance updates within a few seconds, and 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](#suffix-catalog) below.

## Deposit-funded order transfer

A [deposit-funded order](/concepts/deposit-funded-orders) publishes a Conduit-managed funding address in `depositInstructions` instead of debiting a wallet you named. That address has no client-visible wallet id — Conduit owns it and may rotate it — so the **order id** is the handle you fund it with:

`POST https://api.sandbox.conduit.financial/v2/sandbox/orders/{orderId}/deposits/simulate`

<Warning>
  **This endpoint does not behave like the two above, and most of this page does
  not apply to it.** The response is `200 OK` with the **order**, not a deposit.
  The transfer itself still lands as an ordinary `deposit` transaction — readable
  on `GET /v2/transactions`, firing the standard `transaction.*` events — the
  same as any other deposit; see [Deposit-Funded Orders](/concepts/deposit-funded-orders)
  for what it looks like.
</Warning>

**Register the sending address first.** A funding address accepts money only from an address the customer has registered:

```bash theme={null}
curl -X POST "https://api.sandbox.conduit.financial/v2/customers/${CUSTOMER_ID}/wallets/registered-addresses" \
  -H "x-api-key: ${SANDBOX_API_KEY}" \
  -H "idempotency-key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "self_custody",
    "chain": "ethereum",
    "address": "0x8f3a1e5b9c2d4a6f8e0b1c3d5f7a9b1c3d5e7f90",
    "selfCustodyAttestation": true
  }'
```

Screening clears immediately in sandbox (`201`), except for the addresses documented at [Registered addresses](/concepts/registered-addresses) that park for review. Then pass that address as `sourceAddress` when you fund the order.

What differs from the wallet endpoint:

* **`sourceAddress` decides which side of the registration gate you exercise.** Pass a registered address and the funds fund the order. Pass — or default to — an address that is not registered and the funds are sent straight back: the order itself never moves, but the transfer is still a readable `deposit` transaction (it reads `failed`, since nothing was ever credited) and the return is a readable `deposit_return` naming it. `sourceAddress: null` is refused with `400 VALIDATION_ERROR`, because a transfer with no sender has nowhere to go back to.
* **The order's compliance scenarios are keyed on the order, not a deposit id.** `outcome: "frozen" | "returned"` still works on the body, but a transfer held for review is resolved with `POST https://api.sandbox.conduit.financial/v2/sandbox/orders/{orderId}/deposits/simulate/compliance-decision` (below), not the transaction-keyed lever — there is no transaction id to key on.
* **The sender-information gate does not apply, and `originator` is rejected.** Registration replaces it: whether the sending address is registered is the only gate, and sender identity does not open it. Passing `originator` returns `400 VALIDATION_ERROR` rather than being silently ignored — on the wallet route it clears the sender-information gate, and accepting it here would let an unregistered sender fund the order. `DE5E11F1`, the 10,000-unit amount driver, the reserved originator names, and `simulate/sender-info` are all wallet-deposit features with no effect on this route.
* **Send the order's `totalDebit`**, not `sourceAsset.amount` — the order executes only once the funds cover the full debit including fees. Send less and it stays `pending`; send more and the surplus goes back to the sender once the order settles.
* **It accepts the order in any status**, expired and cancelled included. Funding an address after its order lapsed is how an unclaimed transfer arises in production. Note the funding deadline is **5 minutes**, so an order left sitting will already have expired.
* **`assetAmount` must match the order's funding asset** (code *and* chain), else `400 VALIDATION_ERROR`. An order that named its own `source` has no address to fund and returns `409 SANDBOX_ORDER_NOT_DEPOSIT_FUNDED`.

The wallet endpoint above cannot be used for one of these addresses: a Conduit-managed funding address is not a wallet you can name, so `.../wallets/{walletId}/deposits/simulate` returns `404 WALLET_NOT_FOUND` for it, exactly as the live wallet endpoints do.

### Resolving a held funding transfer

When a transfer into a funding address is held for compliance review, resolve it on the order:

`POST https://api.sandbox.conduit.financial/v2/sandbox/orders/{orderId}/deposits/simulate/compliance-decision`

```bash theme={null}
curl -X POST "https://api.sandbox.conduit.financial/v2/sandbox/orders/${ORDER_ID}/deposits/simulate/compliance-decision" \
  -H "x-api-key: ${SANDBOX_API_KEY}" \
  -H "idempotency-key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "outcome": "reject" }'
```

`approve` releases the hold and the funds go on to fund the order; it is available only while the review is still open, and returns `409` once the review has already rejected the transfer — a rejected review can only be rejected. `reject` holds the funds permanently: they neither fund the order nor go back, the order goes unfunded and expires, and the transfer's own transaction reads `failed` with no `failureCode`. `202 Accepted` returns the order as of the call; poll `GET /v2/orders/{orderId}` for the outcome. `404 SANDBOX_ORDER_NO_PARKED_FUNDING` means nothing is currently held for review.

For a full walkthrough — register the sender, create the order, read the address, fund it, watch it auto-execute — see [Offramps](/sandbox/offramps).

## Crypto deposit — sender-information gate

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 `DE5E11F1`. The gate fires regardless of the amount or whether the address is pre-registered.

```bash theme={null}
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": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaade5e11f1"
  }'
```

**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.

<Note>
  **Below-threshold waiver.** When a Travel Rule dollar threshold is configured
  for your environment, a crypto deposit in a US-dollar stablecoin (USDC, USDT,
  PYUSD) whose value is **strictly below** that threshold skips the
  sender-information gate entirely: no
  `transaction.awaiting_sender_information`, no 30-day deadline. The deposit is
  **still compliance-screened** and releases only on a clean screen — so it
  shows a compliance screening on the transaction, then completes. Precedence:
  the `DE5E11F1` suffix (Driver 1) and the 10,000-unit amount driver (Driver 2)
  both still force the gate, so a deposit that trips either one requires sender
  information regardless of the waiver. The waiver applies only when neither
  driver fires.
</Note>

```bash theme={null}
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.

<Note>
  **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.
</Note>

**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 \~10 minutes. 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 \~10 minutes 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`

```bash theme={null}
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.

## Crypto deposit — originator-identity screening

A crypto deposit whose sender is not yet known is compliance-screened **twice**: once on the **source address** the moment it lands (before you provide sender information), and again on the **originator identity** once you supply it. A deposit can therefore clear the source-address screen at arrival — and park on the sender-information gate — yet still be held when the originator identity is screened. While parked on the gate, the deposit already carries a compliance screening for the arrival check; the identity check is a second, independent screening.

To exercise the identity screen in sandbox, provide a **reserved originator name** when you resolve the gate. The name is matched case- and whitespace-insensitively; use a `legalName` (business) or a `firstName`/`lastName` that composes to the reserved value (individual).

```bash theme={null}
# Deposit clears the arrival address screen, parks on sender info, then the
# reserved originator name drives a compliance hold on the enriched identity.
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": "Blocked Sender LLC",
      "country": "USA"
    }
  }'
```

### Reserved originator names (matched on the originator identity)

| Reserved name           | Scenario                                                                                                             | failureCode       |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------- | ----------------- |
| `Blocked Sender LLC`    | compliance hold on the enriched identity — deposit parks for compliance review, then freezes on reject †             | `compliance_hold` |
| `Sanctioned Sender LLC` | sanctions hold on the enriched identity — deposit parks for compliance review, then freezes on reject †              | `compliance_hold` |
| `Risky Sender LLC`      | elevated-risk classification recorded for audit; routes approved at the current threshold (no observable difference) | —                 |
| `Pending Sender LLC`    | compliance review pending on the enriched identity — deposit holds for a compliance decision                         | —                 |

† A held deposit parks for a compliance decision rather than failing outright; resolve it with `POST https://api.sandbox.conduit.financial/v2/sandbox/transactions/{depositId}/simulate/compliance-decision` (`{ "outcome": "reject" }` freezes with `compliance_hold`). Once compliance has rejected the deposit, `{ "outcome": "approve" }` is not available — a rejected case can only be terminalized via `reject`.

A non-reserved originator name does **not** override the source-address (crypto) or account-number (fiat) outcome: the identity screen clears, but if the deposit's address or account number still carries a compliance suffix (for example a rejected or sanctioned scenario), that outcome still applies. A non-reserved name on an otherwise-clean deposit takes the happy path and proceeds.

<Note>
  The reserved originator names match on **any deposit's originator identity**,
  not just crypto. A fiat deposit whose `senderInfo.name` equals a reserved
  value is held on the same identity screen. Use a distinct sender name on fiat
  happy-path tests to avoid tripping it.
</Note>

## 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 compliance 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.

```bash theme={null}
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" }
  }'
```

`202 Accepted` returns the `externalReference` to poll on. 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 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.

<Warning>
  **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.
</Warning>

## 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                             |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| `DEAA49F1`              | compliance approved (explicit happy path)                                                                                                                       | —                                       |
| `DEAA8E51` / `DEAA5A4D` | compliance non-CLEAR (high-risk or sanctions) — deposit holds for a compliance decision, then freezes on reject †                                               | `compliance_hold`                       |
| `DEAA8157`              | compliance risky — no observable difference on the public surface; the deposit completes as APPROVED. The dashboard records the risky classification for audit. | —                                       |
| `DE5E11F1`              | Sender-information gate required — deposit parks; sandbox timer fires after \~10 min                                                                            | `sender_info_timeout` (if not resolved) |

### Fiat deposit suffixes (matched on `senderInfo.accountNumber` digits)

| Suffix                  | Scenario                                                                                                          | failureCode       |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------- | ----------------- |
| `95000000`              | compliance approved (explicit happy path)                                                                         | —                 |
| `95009001` / `95009002` | compliance non-CLEAR (high-risk or sanctions) — deposit holds for a compliance decision, then freezes on reject † | `compliance_hold` |
| `95009003`              | compliance risky — routes approved at current threshold                                                           | —                 |

† On deposits, every non-CLEAR compliance classification (high-risk or sanctions match) holds the deposit for a compliance decision rather than failing it outright — it stays `pending` until the decision lands, and it is never credited in the meantime. Resolve it in sandbox with `POST https://api.sandbox.conduit.financial/v2/sandbox/transactions/{depositId}/simulate/compliance-decision` and `{ "outcome": "reject" }`, which freezes it; `approve` is not available on an already-rejected 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"`.

<Note>
  The `DEAA8157` suffix (crypto) and `95009003` suffix (fiat) trigger an
  elevated-risk compliance 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.
</Note>

## 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 `DE5E11F1` (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 (compliance 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 \~10 minutes so the timeout path is fast to test.

## Errors

See [Errors](/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

```mermaid theme={null}
stateDiagram-v2
    [*] --> Detected : simulate endpoint accepted
    Detected --> ComplianceScreening : ingestion pipeline
    ComplianceScreening --> SenderInfoGate : sourceAddress ends in DE5E11F1 OR amount ≥ 10,000
    ComplianceScreening --> Completed : approved
    ComplianceScreening --> Failed : COMPLIANCE_HOLD
    SenderInfoGate --> Completed : simulate/sender-info called
    SenderInfoGate --> Failed : ~10 min sandbox timer → SENDER_INFO_TIMEOUT
    Completed --> [*]
    Failed --> [*]
```

### Sender-information gate — sandbox auto-pilot timeline

```mermaid theme={null}
sequenceDiagram
    participant You as Your backend
    participant API as Conduit sandbox
    participant WH as Your webhook endpoint

    You->>API: POST .../deposits/simulate (sourceAddress ends DE5E11F1)
    API-->>You: 202 Accepted ({ chain, txHash })
    API-->>WH: transaction.created
    API-->>WH: transaction.awaiting_sender_information<br/>(daysRemaining: ~30, deadlineAt: detectedAt+30days)

    alt Resolve manually (Option B)
        You->>API: POST .../deposits/{id}/simulate/sender-info
        API-->>You: 200 OK (deposit at current state)
        API-->>WH: transaction.completed
    else Wait for auto-timeout (Option A — ~10 min sandbox timer)
        Note over API: ~10 min sandbox timer elapsed
        API-->>WH: transaction.failed (SENDER_INFO_TIMEOUT)
    end
```

## Related pages

* [Sandbox overview](/sandbox/overview) — full sandbox posture and what's mocked
* [Withdrawal failure paths](/sandbox/withdrawals#failure-paths) — compliance magic-suffix catalog for withdrawals and deposits
* [Withdrawals](/sandbox/withdrawals) — crypto withdrawal lifecycle and cosign flows
* [Virtual Accounts](/concepts/virtual-accounts) — virtual account model and deposit instructions
