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.
{
"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"
}
| Field | Meaning |
|---|---|
state | not_onboarded, verification_started, under_review, active, blocked |
can_open_account | Verification approved and the EUR (SEPA) rail enabled — what opening an account needs |
blocked_reason | Present when state is blocked |
next_steps | What 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_at | When 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.
{ "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).
outcome | Status | Meaning |
|---|---|---|
started | 201 | A verification link was issued |
resumed | 201 | The verification already under way, served again |
already_onboarded | 200 | You 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.
| Status | error | When |
|---|---|---|
409 | conflict | The provider already holds a wallet for a customer from an unfinished hosted verification: finish that verification instead |
429 | rate_limited | More than 30 calls an hour for your account |
400 | bad_request | A repeat of an approval that has not completed, more than 24 hours after the first: the provider refuses the reused key |
503 | digital_assets_unavailable | No digital-asset rail on this deployment |
503 | service_unavailable | The provider cannot be reached |
In production the route does not exist (404).
Opening an account
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).
| Status | error | When |
|---|---|---|
409 | digital_assets_onboarding_required | Not approved yet — details: {state, next_steps}. Nothing created |
409 | digital_assets_onboarding_incomplete | The provider holds too little about you to open it (no address, terms not accepted). Complete its verification, then open the account again |
409 | conflict | You already hold an account in this asset |
403 | account_not_verified | Your business is not verified — see KYB. The sandbox lets a pending account through |
403 | currency_limit_reached | Your plan's currency limit is used up. All digital assets together count as one currency |
422 | currency_unavailable | The 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:
{
"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). UseGET /v1/transactionsfor credits andGET /v1/banking/payoutsfor 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.
{
"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.
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.
| Status | error | When |
|---|---|---|
400 | bad_request | No Idempotency-Key |
403 | account_not_verified | Your business is not verified |
409 | digital_assets_onboarding_required, digital_assets_onboarding_incomplete | As for opening an account |
422 | network_unsupported | No asset is offered on that chain here |
503 | digital_assets_unavailable | No 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.
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 most100000.network— optional:solana,base,ethereum,tronortempo. 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.
| Status | error | When |
|---|---|---|
400 | bad_request | No Idempotency-Key; an amount out of range |
404 | not_found | Not one of your accounts |
409 | conflict | The account is not open |
409 | wallet_not_open | No wallet of yours on that network, or on any chain carrying the asset |
422 | account_not_digital | A bank account — use the faucet |
422 | asset_not_on_network | The asset is not offered on that chain |
429 | rate_limited | More than 30 requests an hour for your account |
503 | digital_assets_unavailable | No 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:
"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
}
| Field | Means |
|---|---|
send_onchain | Chains the account can send its asset from — your wallets' chains that carry it. [] while sends are off |
send_onchain_to | Chains a destination address may be on: your wallets' own and the cross-chain destinations, open to your business's region. Sorted |
send_internal_to | Chains 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_bank | The 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_to | The other of USDC and EURC, where you hold that account too on a chain both share (solana, base, tempo) |
test_funds | Test funds can be simulated into it |
trusted_beneficiaries_required_for_api_keys | true where an API key sends to third parties only to a trusted beneficiary. Absent where there are no sends |
trusted_beneficiary_cooldown_seconds | How 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.