Skip to main content
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. Inbound, there is no difference.
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 and Virtual Accounts. Do not look for crypto in a Virtual Account. It will not be there.

Prerequisites

  • An onboarded customer. See Onboard a Customer.
  • The crypto wallets feature active on that customer.
  • A webhook endpoint subscribed to the crypto_wallet.* and transaction.* events. See 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")

Deposit settles                 → transaction.completed · balance moves pending → available


Terminal state                  → completed · failed
Supported chains (lowercase): ethereum, base, polygon, solana, tron. Amounts use the Money shape. Echo what Conduit sends you. Never round-trip an amount through a float.

Step 1 — Provision a wallet with a usable address

1

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:
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.
2

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 covers that branch. Whether custodial or non-custodial, the same event tells you the address is live.
3

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:
curl https://api.conduit.financial/v2/customers/{customerId}/wallets/{walletId} \
  -H "x-api-key: YOUR_API_KEY"
{
  "id": "wlt_2xKjF9mQb7vN4hL1pR3w8t",
  "chain": "ethereum",
  "address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18",
  "balances": []
}
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.

Step 2 — Share the address with the sender

1

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

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

Step 3 — Funds arrive and Conduit opens a deposit

1

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

Advance: nothing — react to the webhook

There is nothing to do. Listen for the deposit event and reconcile it against your records.
3

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 }).
{
  "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" }
  }
}
The unregistered-sender branch. If the source address is not registered, the deposit does not settle. It parks and fires transaction.awaiting_sender_information instead of completing. The payload carries the sourceAddress, the assetAmount, and an expiresAt / deadlineAt deadline — a 30-day window. Conduit needs the sender’s originator details to satisfy the Travel Rule — the requirement to exchange originator and beneficiary information on transfers. Submit them to POST /v2/transactions/{transactionId}/sender-information before the deadline. Pass register: true to also save the address, so future deposits from it clear on their own. See Registered Addresses for the full originator shape, which covers both individual and business senders. If the window elapses, the deposit times out and fails with failureCode: SENDER_INFO_TIMEOUT. See SENDER_INFO_TIMEOUT.
What can go wrong
  • The source address is unregistered → the deposit parks at transaction.awaiting_sender_information; submit sender information before the deadline, or it fails with SENDER_INFO_TIMEOUT. See SENDER_INFO_TIMEOUT.
  • The deposit is reversed or returned before it is credited → transaction.failed with failureCode: RETURNED_BY_SENDER. See RETURNED_BY_SENDER.
  • A compliance review parks or holds the deposit → transaction.failed with failureCode: COMPLIANCE_HOLD. See COMPLIANCE_HOLD.
  • The source is sanctioned → transaction.failed with failureCode: AML_SANCTIONED. See AML rejections.

Step 4 — The deposit settles and the balance is credited

1

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

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

Step 5 — Read the balance back

1

State: funds are credited and spendable

The deposit lands on the wallet’s balances[], one entry per asset. Each entry carries three buckets:
BucketMeaning
availableSpendable now — what a payout can draw on.
pendingDetected but not yet settled; not spendable.
frozenHeld by a compliance action; not spendable.
On detection (Step 3) the amount sits in pending. On settlement (Step 4) it moves to available. A rejected or held deposit moves to frozen.
2

Advance: read the wallet

Read the wallet and confirm available reflects the deposit before you spend it.
curl https://api.conduit.financial/v2/customers/{customerId}/wallets/{walletId} \
  -H "x-api-key: YOUR_API_KEY"
{
  "id": "wlt_2xKjF9mQb7vN4hL1pR3w8t",
  "chain": "ethereum",
  "address": "0x742d35cc6634c0532925a3b844bc9e7595f2bd18",
  "balances": [
    { "code": "USDC", "available": "1000.000000", "pending": "0", "frozen": "0" }
  ]
}
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, or initiate a custodial payout the same way.

Terminal state

An inbound deposit stops at one of two terminal states.
Terminal statusWebhookMeaning
completedtransaction.completedFunds settled and the amount is in available. The on-chain txHash is on the external_crypto side. The journey is done.
failedtransaction.failedThe deposit could not be credited. Branch on failureCodeRETURNED_BY_SENDER, COMPLIANCE_HOLD, AML_SANCTIONED, or SENDER_INFO_TIMEOUT. When failureCode is absent the cause isn’t actionable — contact support.

Where to go next