Skip to main content

Accounts and wallets

Sandbox only. The routes here take a session or an API key with the scope shown in Who may do what.


Onboarding​

A digital-asset account needs your business verified with our crypto-asset service provider — separately from your bank verification.

GET /v1/banking/kyb/digital-assets​

Where you stand, from our record. ?refresh=true confirms it with the provider first. Key scope accounts:read.

JSON
{
"state": "active",
"can_open_account": true,
"blocked_reason": null,
"next_steps": ["Open a USDC account at POST /v1/banking/accounts with currency USDC."],
"rails": [
{ "rail": "EUR (SEPA)", "status": "enabled", "outstanding": [] },
{ "rail": "USD (ACH, wire, FedNow)", "status": "enabled", "outstanding": [] }
],
"confirmed_at": "2026-10-25T09:12:00Z"
}
FieldMeaning
statenot_onboarded, verification_started, under_review, active, blocked
can_open_accountVerification approved and the EUR (SEPA) rail enabled — what opening an account needs
blocked_reasonPresent when state is blocked
next_stepsWhat to do next, in plain words. Empty when nothing is left: once can_open_account is true and your USDC account is open. Until it is open, it says to open it
rails[]Each rail by currency, once: status not_requested, pending, enabled, withdrawn or unknown, and what is outstanding. A bank send outside SEPA is offered only over an enabled rail
confirmed_atWhen our record was last confirmed with the provider

POST /v1/banking/kyb/digital-assets​

Starts the provider's hosted verification. Key scope accounts:write; Idempotency-Key required.

JSON
{ "redirect_url": "https://app.sandbox.cashxchain.com/digital-assets", "account_type": "business" }

redirect_url is where the person lands afterwards: https, on one of this platform's own origins. account_type is business (default) or individual. The legal name and email come from your account.

The answer is {outcome, verification_url, terms_url, onboarding} — onboarding as in the GET. Send the person to verification_url (and terms_url when set).

outcomeStatusMeaning
started201A verification link was issued
resumed201The verification already under way, served again
already_onboarded200You can open your USDC account, or already hold it (then next_steps is empty); verification_url is null

In the sandbox you do not need this route — the approval below works whether or not you started or finished the hosted form.

POST /v1/sandbox/banking/kyb/digital-assets/approve — sandbox only​

Approves you at the provider's sandbox and answers 200 with the status, as in the GET above — can_open_account: true once the EUR (SEPA) rail is enabled. No body, no Idempotency-Key. Key scope accounts:write.

  • No provider customer yet (the hosted form never started, or not tied to a customer): a test customer is made and approved.
  • A hosted verification you completed in the browser: the customer it produced is approved.
  • One you started and did not finish: a test customer is made and approved in its place. You need not open the old link again.

The rails. Besides EUR (SEPA) and USD (ACH, wire, FedNow), the approval requests and approves GBP (Faster Payments), MXN (SPEI) and BRL (Pix) — the rails gb_bank, clabe and pix sends are paid over. rails then normally lists all five enabled. If the provider does not take the request, the approval still goes through with the rails you already hold: check rails. If it does not list GBP (Faster Payments), MXN (SPEI) and BRL (Pix) as enabled (an account approved before these rails were added), call the route again to add them. Your region still decides: GBP, MXN and BRL stay closed to a business in the EEA whatever rails says.

Calling it again in the same state repeats nothing.

StatuserrorWhen
409conflictThe provider already holds a wallet for a customer from an unfinished hosted verification: finish that verification instead
429rate_limitedMore than 30 calls an hour for your account
400bad_requestA repeat of an approval that has not completed, more than 24 hours after the first: the provider refuses the reused key
503digital_assets_unavailableNo digital-asset rail on this deployment
503service_unavailableThe provider cannot be reached

In production the route does not exist (404).


Opening an account​

Bash
curl -s -X POST "$CXC_BASE/v1/banking/accounts" \
-H "Authorization: Bearer $CXC_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: da-open-eurc-$RUN" \
-d '{"currency": "EURC"}'

currency is one of USDC, EURC, USDT, USDB, PYUSD, PATHUSD — those listed with kind digital in offered_currencies of GET /v1/banking/accounts. One account per asset. The account is the asset on every chain where you already hold a wallet that carries it; where none does, a wallet is opened on the asset's first chain (Solana, or Tempo for PATHUSD).

StatuserrorWhen
409digital_assets_onboarding_requiredNot approved yet — details: {state, next_steps}. Nothing created
409digital_assets_onboarding_incompleteThe provider holds too little about you to open it (no address, terms not accepted). Complete its verification, then open the account again
409conflictYou already hold an account in this asset
403account_not_verifiedYour business is not verified — see KYB. The sandbox lets a pending account through
403currency_limit_reachedYour plan's currency limit is used up. All digital assets together count as one currency
422currency_unavailableThe asset is not offered here

What the account looks like​

GET /v1/banking/accounts lists a digital-asset account twice: in currency_accounts with kind digital, and in accounts with currency set to the asset and these extras (an accounts entry has no kind):

  • identifiers — one per wallet that carries the asset, in the order of the chain table: {"scheme": "onchain", "network": "base", "asset": "EURC", "address": "0x…"}. The USDC account lists its EUR IBAN (with BIC) first.
  • digital_actions — what the account can do; below.
  • own_account_payouts — ["EUR"] on the USDC account where sending back to your EUR account is offered; [] on every other digital-asset account. See Sending.

Balance — GET /v1/banking/accounts/{account_id}/balance reports the asset over every wallet that carries it, and each wallet's share:

JSON
{
"balances": [
{
"account_id": "…",
"currency": "EURC",
"available": "4.750000",
"pending": "0",
"as_of": "2026-11-03T09:12:00Z",
"by_network": [
{ "network": "solana", "address": "…", "available": "3.250000" },
{ "network": "base", "address": "0x…", "available": "1.500000" }
]
}
],
"refreshing": false
}

available is the sum of by_network, within the one asset. Amounts carry the asset's six decimal places. by_network is [] on a bank account. Money held by sends still in flight, or by one for about a minute after it finishes, is not taken off this figure: a send can be refused 400 insufficient_balance with less free than it shows — see Sandbox behaviour.

Pay-in details — GET /v1/banking/accounts/{account_id}/payin-instructions has one on-chain instruction per wallet that carries the asset (rail is the chain, currency the asset, notice names both). The USDC account adds its SEPA instruction first. The SEPA payee name is the IBAN's holder exactly as the provider states it; when that is not your company's name, beneficiary_is_account_holder is false and notice tells the payer to enter the name as shown. In the sandbox nothing reaches these details — no SEPA transfer and no on-chain transfer is credited; fund the account with test funds.

A credit in an asset you have not opened is held, then booked when you open it. EURC sent to your Solana address before you hold an EURC account shows no balance and no payin.received; opening the EURC account books it within seconds, with its webhook.

What a digital-asset account does not do​

  • Statements — not available for digital-asset accounts yet (400 bad_request, naming the account's asset). Use GET /v1/transactions for credits and GET /v1/banking/payouts for sends.
  • FX — no FX quote or conversion at the account; between USDC and EURC use a swap.
  • Closing — the USDC account cannot be closed (422 account_not_closable), and an open USDC account blocks closing your whole CashXChain account. A plan downgrade never closes a digital-asset account.

Wallets​

GET /v1/banking/digital-assets/wallets — sandbox only​

Your wallets, each read from the provider now, and every chain offered. Key scope accounts:read.

JSON
{
"wallets": [
{ "network": "solana", "address": "…", "opened_at": "2026-10-28T09:00:00Z",
"assets": ["USDC", "EURC", "USDT", "USDB", "PYUSD"],
"balances": [ { "asset": "USDC", "available": "125.500000" },
{ "asset": "EURC", "available": "3.250000" } ] }
],
"networks": [
{ "network": "solana", "assets": ["USDC", "EURC", "USDT", "USDB", "PYUSD"], "opened": true },
{ "network": "base", "assets": ["USDC", "EURC", "USDB"], "opened": false }
]
}

balances lists every asset offered on the wallet's chain, each in its own asset; one the wallet never held reads 0. Nothing is summed across assets.

POST /v1/banking/digital-assets/wallets — sandbox only​

Opens your wallet on a chain. Key scope accounts:write; Idempotency-Key required.

Bash
curl -s -X POST "$CXC_BASE/v1/banking/digital-assets/wallets" \
-H "Authorization: Bearer $CXC_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: da-wallet-base-$RUN" \
-d '{"network": "base"}' | jq

201 with the wallet (one element of wallets above), or 200 with the wallet you already hold there — a new key never opens a second one. The new wallet's address is added to every account whose asset its chain carries.

StatuserrorWhen
400bad_requestNo Idempotency-Key
403account_not_verifiedYour business is not verified
409digital_assets_onboarding_required, digital_assets_onboarding_incompleteAs for opening an account
422network_unsupportedNo asset is offered on that chain here
503digital_assets_unavailableNo digital-asset rail on this deployment

Test funds​

POST /v1/sandbox/banking/accounts/{account_id}/simulate-deposit — sandbox only​

Deposits the account's own asset into one of your wallets through the provider's sandbox. Session, or a key with payments:write; Idempotency-Key required.

Bash
curl -s -X POST "$CXC_BASE/v1/sandbox/banking/accounts/$USDC/simulate-deposit" \
-H "Authorization: Bearer $CXC_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: da-test-funds-base-$RUN" \
-d '{"amount": "50", "network": "base"}'
  • amount — in the asset, positive, at most six decimal places, at most 100000.
  • network — optional: solana, base, ethereum, tron or tempo. Left out, the deposit lands on your Solana wallet where it carries the asset, else on the first wallet you opened that does.

202 — {"account_id", "currency", "network", "amount", "status": "simulated"}, network being the chain it was simulated on. Nothing is booked by this call: the deposit is read from the provider and booked like any credit, usually within five seconds (at most five minutes) — the balance, an onchain_transfer row on GET /v1/transactions, and payin.received.

The same key with the same amount and network is the same deposit — it is not deposited twice. To repeat a request that named no network exactly, name the network the first answer reported.

Test funds are always credited, whatever you sent before. Other credits the size of a recent send can be held as a possible refund — see Sandbox behaviour.

StatuserrorWhen
400bad_requestNo Idempotency-Key; an amount out of range
404not_foundNot one of your accounts
409conflictThe account is not open
409wallet_not_openNo wallet of yours on that network, or on any chain carrying the asset
422account_not_digitalA bank account — use the faucet
422asset_not_on_networkThe asset is not offered on that chain
429rate_limitedMore than 30 requests an hour for your account
503digital_assets_unavailableNo digital-asset rail, or no sandbox deposits at its provider

EUR by SEPA into the USDC account's IBAN cannot be simulated, and no real or testnet asset sent to a sandbox address is ever credited. To show EUR arriving as USDC, use the convert where it is offered. In the web app: Digital Assets → Add test funds.


digital_actions​

Every digital-asset account in GET /v1/banking/accounts carries digital_actions — what it offers, from configuration and our records:

JSON
"digital_actions": {
"send_onchain": ["solana", "base"],
"send_onchain_to": ["arbitrum", "avalanche_c_chain", "base", "celo", "ethereum", "optimism", "polygon", "solana", "stellar"],
"send_internal_to": ["base", "ethereum", "solana"],
"send_iban": ["EUR"],
"send_bank": ["us_bank"],
"swap_to": ["EURC"],
"test_funds": true,
"trusted_beneficiaries_required_for_api_keys": true,
"trusted_beneficiary_cooldown_seconds": 0
}
FieldMeans
send_onchainChains the account can send its asset from — your wallets' chains that carry it. [] while sends are off
send_onchain_toChains a destination address may be on: your wallets' own and the cross-chain destinations, open to your business's region. Sorted
send_internal_toChains another customer's wallet can be reached on by a send by @handle — which one is ours to choose. Sorted
send_iban["EUR"] where the asset reaches a SEPA IBAN (USDC, EURC); else []
send_bankThe bank-account destinations outside SEPA, as destination.type: us_bank, gb_bank, clabe, pix — those open to your business's region, and paid over a rail the provider has enabled for you (rails). A business in the EEA sees us_bank alone
swap_toThe other of USDC and EURC, where you hold that account too on a chain both share (solana, base, tempo)
test_fundsTest funds can be simulated into it
trusted_beneficiaries_required_for_api_keystrue where an API key sends to third parties only to a trusted beneficiary. Absent where there are no sends
trusted_beneficiary_cooldown_secondsHow long after an approval an API key may use it. Present with the field above

It says what is offered, not that a request will be accepted: the amount, the destination and the provider are checked when you send. The deployment-wide view is Capabilities.