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

# Receive Crypto Lifecycle

> The journey of an inbound crypto deposit: provision a wallet, share its address, watch funds arrive on-chain, see the balance credited, and follow the deposit to a terminal state

Crypto arrives at Conduit in order. You create a wallet and wait for it to provision. You share that wallet's address with whoever is sending you funds. The funds arrive on-chain, Conduit detects them and opens a deposit, the deposit settles, and the balance lands on the wallet.

Each step below answers four questions: **what state you're in**, **what you do to advance**, **what webhook fires**, and **what can go wrong**. Receiving is submit-and-listen. Once the address exists there is nothing to call. You react to webhooks. You can poll the wallet's `GET` endpoint, but you should never have to.

Receiving works **identically for custodial and non-custodial wallets**. The deposit address is the wallet address either way. Detection and settlement are the same, and the balance lands in the same place. Custody only changes who controls the keys when funds *leave* — see the [Non-Custodial Payout Lifecycle](/guides/non-custodial-payout-lifecycle). Inbound, there is no difference.

<Note>
  Virtual Accounts are **fiat-only**. Crypto never credits a Virtual Account —
  an inbound deposit always credits the destination **wallet**. Funding with
  dollars over a bank rail is a different story: see the [Money Movement
  Lifecycle](/guides/money-movement-lifecycle) and [Virtual
  Accounts](/concepts/virtual-accounts). Do not look for crypto in a Virtual
  Account. It will not be there.
</Note>

<Note>
  **Test this flow in sandbox.** Drive it end-to-end with simulated money and
  deterministic controls — start with the [sandbox
  quickstart](/sandbox/quickstart), then [deposit simulation](/sandbox/deposits)
  for this flow, and the [cheat sheet](/sandbox/cheat-sheet) for every magic
  value and simulate endpoint.
</Note>

## Prerequisites

* An onboarded customer. See [Onboard a Customer](/guides/onboard-customer).
* The crypto wallets feature active on that customer.
* A webhook endpoint subscribed to the `crypto_wallet.*` and `transaction.*` events. See [Webhooks](/webhooks).

## The journey at a glance

```
Create a wallet                 → address is null until provisioning finishes
        │
        ▼
Wallet finishes provisioning    → crypto_wallet.completed · address is now populated
        │
        ▼
Share the address               → the wallet address IS the deposit address (no call)
        │
        ▼
Funds arrive on-chain            → deposit transaction: pending
        │                           (transaction.created, type: "deposit")
        ▼
Unregistered sender AND (stablecoin ≥ $3,000, or a non-pegged asset)?  (Travel Rule)
   │
   ├─ YES ─▶ Deposit PARKS — funds are NOT credited, NOT spendable
   │         transaction.awaiting_sender_information  (30-day deadline)
   │              │
   │              ▼
   │         You MUST submit sender information  → POST /v2/transactions/{id}/sender-information
   │              │                                 (miss the deadline → deposit FAILS: SENDER_INFO_TIMEOUT)
   │              ▼  (accepted)
   └─ NO ──▶ Deposit settles           → transaction.completed · balance moves pending → available
             (registered sender, or stablecoin under $3,000)
                  │
                  ▼
             Terminal state            → completed · failed
```

Supported chains (lowercase): `ethereum`, `base`, `polygon`, `solana`, `tron`. Amounts use the [Money shape](/concepts/money). Echo what Conduit sends you. Never round-trip an amount through a float.

## Step 1 — Provision a wallet with a usable address

<Steps>
  <Step title="State: no wallet, or a wallet whose address is still null">
    A deposit needs somewhere to land. Create a wallet on the chain you want to receive on:

    ```bash theme={null}
    curl https://api.conduit.financial/v2/customers/{customerId}/wallets \
      -X POST \
      -H "x-api-key: YOUR_API_KEY" \
      -H "idempotency-key: $(uuidgen)" \
      -H "content-type: application/json" \
      -d '{ "chain": "ethereum" }'
    ```

    The wallet (id prefix `wlt_`) is created immediately, but its `address` is **`null` while it provisions**. Conduit assigns the address when provisioning finishes. There is no separate "generate deposit address" call. The address is a property of the wallet. A wallet without an address cannot receive anything.
  </Step>

  <Step title="Advance: wait for provisioning to finish">
    Provisioning runs in the background. There is no call to make. You wait for one webhook.

    For non-custodial wallets, provisioning also depends on the customer's signers enrolling. The [Non-Custodial Payout Lifecycle](/guides/non-custodial-payout-lifecycle) covers that branch. Whether custodial or non-custodial, the same event tells you the address is live.
  </Step>

  <Step title="Webhook: crypto_wallet.completed">
    `crypto_wallet.completed` fires when provisioning finishes and the wallet is ready to receive funds. After it, the wallet's `address` is populated and usable. Read it back at any time:

    ```bash theme={null}
    curl https://api.conduit.financial/v2/customers/{customerId}/wallets/{walletId} \
      -H "x-api-key: YOUR_API_KEY"
    ```

    ```json theme={null}
    {
      "id": "wlt_2xKjF9mQb7vN4hL1pR3w8t",
      "chain": "ethereum",
      "address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18",
      "balances": []
    }
    ```
  </Step>
</Steps>

<Warning>
  Do not share an address while it is `null`. Gate sharing on
  `crypto_wallet.completed` (or on a `GET` that returns a non-null `address`)
  rather than reading the field right after creation. Funds sent before an
  address exists have nowhere to land.
</Warning>

## Step 2 — Share the address with the sender

<Steps>
  <Step title="State: the wallet has a populated address">
    The wallet's `address` field **is** the deposit address. There is nothing to
    generate, register, or activate. Hand that one string to whoever is sending
    you crypto, along with the chain. An address is only valid on its own chain.
  </Step>

  <Step title="Advance: send the address out of band, then wait">
    Give the sender the `address` and the `chain`. From here there is nothing to
    call. There is **no API call to start a deposit** — like the fiat funding
    story, you do not tell Conduit a deposit is coming. Conduit watches the
    chain and opens the deposit when funds arrive.
  </Step>
</Steps>

<Note>
  One address per wallet, reusable for every deposit on that chain. You do not
  need a fresh address per payment. To receive on a different chain, create
  another wallet for it (Step 1).
</Note>

## Step 3 — Funds arrive and Conduit opens a deposit

<Steps>
  <Step title="State: funds confirmed on-chain, deposit pending">
    When the sender's transfer confirms on-chain, Conduit detects it and opens a deposit transaction in status `pending`. The credited-but-not-final amount appears in the wallet's `balances[]` under `pending`.
  </Step>

  <Step title="Advance: nothing — react to the webhook">
    There is nothing to do. Listen for the deposit event and reconcile it against your records.
  </Step>

  <Step title="Webhook: transaction.created">
    `transaction.created` fires with `type: "deposit"`. The `source` is `type: "external_crypto"` and carries the sender's `address`; the `destination` is `type: "wallet"` and carries the `walletId`, the wallet `address`, and the `assetAmount` (`{ code, chain, amount }`).

    ```json theme={null}
    {
      "transactionId": "txn_2xKjF9mQb7vN4hL1pR3w8t",
      "customerId": "cus_2xKjF9mQb7vN4hL1pR3w8t",
      "type": "deposit",
      "source": {
        "type": "external_crypto",
        "address": "0x742d35cc6634c0532925a3b8d4c9c2c7a3b3d7e1",
        "assetAmount": { "code": "USDC", "chain": "ethereum", "amount": "1000.000000" }
      },
      "destination": {
        "type": "wallet",
        "walletId": "wlt_2xKjF9mQb7vN4hL1pR3w8t",
        "address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18",
        "assetAmount": { "code": "USDC", "chain": "ethereum", "amount": "1000.000000" }
      }
    }
    ```
  </Step>
</Steps>

<Note>
  A side can also be `type: "deposit_address"` — a Conduit-provided funding
  address. It carries `address` and `assetAmount` but **no `walletId`**, so
  handle it as its own case rather than reading `walletId` off it. See
  [Deposit-Funded Orders](/concepts/deposit-funded-orders).
</Note>

**What can go wrong**

* A compliance review parks or holds the deposit → `transaction.failed` with `failureCode: compliance_hold`. See [COMPLIANCE\_HOLD](/errors/compliance-hold).

## Step 4 — Clear an unregistered sender (required at or above the \$3,000 Travel Rule threshold)

**The under-threshold shortcut applies only to USD-pegged stablecoins (USDC, USDT).** A **stablecoin** deposit \*\*under $3,000** credits straight through with no sender information required — even from a never-seen address. This is how Conduit satisfies the **Travel Rule**, the regulatory requirement to exchange originator and beneficiary information on a transfer at or above that threshold. The $3,000 shortcut is valued at the stablecoin's 1:1 USD peg; any deposit that is **not** a USD-pegged stablecoin takes the sender-information path below regardless of amount.

**When this step applies, it is mandatory.** A qualifying deposit (a stablecoin of \$3,000 or more, or any non-pegged asset, from a sender Conduit hasn't seen before) does **not** settle on its own. It **parks**: the amount sits in `pending`, is **not** credited to the wallet, and **cannot be spent** until you submit the sender's details. Miss the deadline and the deposit fails for good — the funds are never usable. (A parked deposit can also still be held or rejected by AML review — see [COMPLIANCE\_HOLD](/errors/compliance-hold).)

If the sender's address is already registered — **or the deposit is a stablecoin under \$3,000** — this step does not happen; the deposit goes straight from Step 3 to settlement (Step 5).

<Steps>
  <Step title="State: deposit parked, awaiting sender information">
    Instead of settling, the deposit holds in `pending` and Conduit fires `transaction.awaiting_sender_information`. Nothing is credited while it is parked. The fields you act on are the `sourceAddress` you must register, the `assetAmount`, the `deadlineAt`, and the `daysRemaining` countdown — a **30-day** window in live. The example below is abbreviated to those; see [Webhooks](/webhooks) for the complete payload.

    ```json theme={null}
    {
      "transactionId": "txn_2xKjF9mQb7vN4hL1pR3w8t",
      "customerId": "cus_2xKjF9mQb7vN4hL1pR3w8t",
      "sourceAddress": "0x742d35cc6634c0532925a3b8d4c9c2c7a3b3d7e1",
      "assetAmount": { "code": "USDC", "chain": "ethereum", "amount": "1000.000000" },
      "deadlineAt": "2026-02-14T09:30:00.000Z",
      "daysRemaining": 30
    }
    ```
  </Step>

  <Step title="Advance: submit the sender's details before the deadline">
    Submitting the originator information for the source address is the **only** way to release a parked deposit. There is no other call, and the funds stay locked in `pending` until you do.

    ```bash theme={null}
    curl https://api.conduit.financial/v2/transactions/{id}/sender-information \
      -X POST \
      -H "x-api-key: YOUR_API_KEY" \
      -H "content-type: application/json" \
      -d '{
        "originator": { ... },
        "register": true
      }'
    ```

    Pass `register: true` to also save the address so future deposits from it clear on their own; leave it out (or `false`) to clear only this deposit. See [Registered Addresses](/concepts/registered-addresses) for the full `originator` shape — it covers both individual and business senders. You can submit only once per deposit: a second submission for the same deposit returns `409`.

    <Warning>
      **Double-check the originator before you submit — the API accepts one submission per deposit.** A second call returns `409 SENDER_INFO_ALREADY_RECORDED`, so a typo in the sender's name or address cannot be fixed through the API. If you submitted incorrect details, contact Conduit to have them corrected while the deposit is still under review.
    </Warning>
  </Step>

  <Step title="Outcome: the deposit resumes — or times out">
    Once your submission is accepted, the deposit leaves the parked state and continues to settlement (Step 5), exactly as a registered-sender deposit would. If the 30-day window elapses with no submission, the deposit is terminated: `transaction.failed` with `failureCode: sender_info_timeout`, and the funds are never credited. See [SENDER\_INFO\_TIMEOUT](/errors/sender-info-timeout).
  </Step>
</Steps>

<Warning>
  **A parked deposit is not usable.** The amount stays in `pending` and never
  moves to `available` until you submit sender information for the source
  address. There is no way to spend, convert, or withdraw it first, and once the
  30-day deadline passes the deposit fails permanently. Treat
  `transaction.awaiting_sender_information` as a required action, not a
  notification.
</Warning>

## Step 5 — The deposit settles and the balance is credited

<Steps>
  <Step title="State: completed">
    Once the deposit clears, it settles. The amount moves out of `pending` and
    into `available` on the wallet's matching per-asset balance. The deposit
    transaction is now terminal.
  </Step>

  <Step title="Webhook: transaction.completed">
    `transaction.completed` fires for the deposit. The on-chain settlement
    reference, `txHash`, lives on the `external_crypto` side of the payload. The
    public transaction status has moved `pending` → `completed`.
  </Step>
</Steps>

## Step 6 — Read the balance back

<Steps>
  <Step title="State: funds are credited and spendable">
    The deposit lands on the **wallet's** `balances[]`, one entry per asset. Each entry carries three buckets:

    | Bucket      | Meaning                                      |
    | ----------- | -------------------------------------------- |
    | `available` | Spendable now — what a payout can draw on.   |
    | `pending`   | Detected but not yet settled; not spendable. |
    | `frozen`    | Held by a compliance action; not spendable.  |

    On detection (Step 3) the amount sits in `pending`. On settlement (Step 5) it moves to `available`. A rejected or held deposit moves to `frozen`.
  </Step>

  <Step title="Advance: read the wallet">
    Read the wallet and confirm `available` reflects the deposit before you spend it.

    ```bash theme={null}
    curl https://api.conduit.financial/v2/customers/{customerId}/wallets/{walletId} \
      -H "x-api-key: YOUR_API_KEY"
    ```

    ```json theme={null}
    {
      "id": "wlt_2xKjF9mQb7vN4hL1pR3w8t",
      "chain": "ethereum",
      "address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18",
      "balances": [
        { "code": "USDC", "available": "1000.000000", "pending": "0", "frozen": "0" }
      ]
    }
    ```
  </Step>
</Steps>

<Note>
  Spend against `available` only. The balance lands on the wallet, never on a
  Virtual Account — Virtual Accounts hold fiat. To send the received crypto back
  out, follow the [Non-Custodial Payout
  Lifecycle](/guides/non-custodial-payout-lifecycle), or initiate a custodial
  payout the same way.
</Note>

## Terminal state

An inbound deposit stops at one of two terminal states.

| Terminal status | Webhook                 | Meaning                                                                                                                                                                             |
| --------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `completed`     | `transaction.completed` | Funds settled and the amount is in `available`. The on-chain `txHash` is on the `external_crypto` side. The journey is done.                                                        |
| `failed`        | `transaction.failed`    | The deposit could not be credited. Branch on `failureCode` — `compliance_hold` or `SENDER_INFO_TIMEOUT`. When `failureCode` is absent the cause isn't actionable — contact support. |

## Where to go next

* [Crypto Wallets](/concepts/crypto-wallets) — the wallet resource, custody models, and key rotation
* [Non-Custodial Wallets](/concepts/non-custodial-wallets) — co-signing outbound transfers
* [Non-Custodial Payout Lifecycle](/guides/non-custodial-payout-lifecycle) — sending received crypto back out
* [Money Movement Lifecycle](/guides/money-movement-lifecycle) — the fiat-funding twin of this story
* [Webhooks](/webhooks) — subscribe to `crypto_wallet.completed` and `transaction.*`
