Quickstart: your first USDC send
This page goes from a sandbox secret key to a USDC send settled on chain, as the calls you make, in order. Everything here is sandbox only.
You need curl, jq, a sandbox account at
auth.sandbox.cashxchain.com
with two-factor authentication set up (the web app asks for it before any
payment), and a secret key from the sandbox console,
dev.sandbox.cashxchain.com → API keys
→ Create key.
export CXC_BASE=https://api.sandbox.cashxchain.com
export CXC_KEY=cxc_sk_sandbox_... # your secret key
export RUN=$(date +%s) # makes this run's Idempotency-Keys unique
Every write below carries an Idempotency-Key. Sending the same request
again under the same key is safe — it answers with what the first one did —
so keep $RUN while you retry, and change it for a new operation. See
Idempotency.
Step 1 — See what is offered
curl -s "$CXC_BASE/v1/banking/capabilities" \
-H "Authorization: Bearer $CXC_KEY" | jq '{environment, flows: [.flows[] | {flow, status}]}'
environment is sandbox, and the digital-asset flows read sandbox_only,
or requires_onboarding until Step 2 is done. Every field is explained in
Capabilities.
Step 2 — Get approved
A digital-asset account needs your business verified with our crypto-asset service provider. In the sandbox one call does it — no hosted form:
curl -s -X POST "$CXC_BASE/v1/sandbox/banking/kyb/digital-assets/approve" \
-H "Authorization: Bearer $CXC_KEY" | jq '{state, can_open_account, next_steps}'
{
"state": "active",
"can_open_account": true,
"next_steps": ["Open a USDC account at POST /v1/banking/accounts with currency USDC."]
}
No body and no Idempotency-Key; calling it again is harmless. In the web
app, Open USDC account on the Digital Assets page approves you and
opens the account (Steps 2 and 3) in one click. Details:
Onboarding.
Step 3 — Open a USDC 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-usdc-$RUN" \
-d '{"currency": "USDC"}' | jq '{currency_account, error, message}'
USDC=$(curl -s "$CXC_BASE/v1/banking/accounts" \
-H "Authorization: Bearer $CXC_KEY" \
| jq -r '.accounts[] | select(.currency == "USDC") | .id')
echo "$USDC"
The account opens with a wallet on Solana, and lists two identifiers: an EUR
IBAN and its Solana address. 409 digital_assets_onboarding_required means
Step 2 has not finished; 409 conflict means you already hold one — carry on
with its id.
Step 4 — Add test funds
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-$RUN" \
-d '{"amount": "100", "network": "solana"}' | jq
{
"account_id": "…",
"currency": "USDC",
"network": "solana",
"amount": "100.000000",
"status": "simulated"
}
The 202 means the provider's sandbox was asked to deposit. The deposit is
read and booked within seconds — a balance, an onchain_transfer row on
GET /v1/transactions, and a payin.received
webhook:
curl -s "$CXC_BASE/v1/banking/accounts/$USDC/balance" \
-H "Authorization: Bearer $CXC_KEY" | jq
Test funds are always credited, whatever you sent before. They are how you
fund a digital-asset account from outside: no real or testnet asset sent to a
sandbox address is credited. The faucet
(POST /v1/sandbox/banking/faucet) is for bank accounts only: for USDC it
answers 422 asset_unsupported.
Step 5 — Approve a recipient
An API key sends to a third party only to a trusted beneficiary — a destination a person of your account approved, with their second factor. No key can approve one. Approve one in the web app:
- Sign in at app.sandbox.cashxchain.com.
- On the Digital Assets page, on the Approved recipients card, choose Manage, then Approve a recipient.
- Give it a name, and enter a Solana wallet address — not one of your own — and who holds it: for a third party, the name, street, city and country (the travel rule). Confirm with your second factor.
The sandbox lets API keys use an approval at once: its cooldown is 0 —
trusted_beneficiary_cooldown_seconds in GET /v1/banking/accounts says
what the deployment runs with. Read the approval's id with your key:
TRUSTED=$(curl -s "$CXC_BASE/v1/banking/digital-assets/trusted-beneficiaries" \
-H "Authorization: Bearer $CXC_KEY" \
| jq -r '[.trusted_beneficiaries[] | select(.status == "active" and .kind == "onchain")][0].id')
echo "$TRUSTED"
The approval itself — the request the web app sends, and every kind of destination — is in API keys and trusted beneficiaries.
Step 6 — Quote the send and lock its route
A quote moves nothing. It checks the send as the payout will, answers the
provider's minimum, and locks the route under a quote_id:
QUOTE=$(curl -s -X POST "$CXC_BASE/v1/banking/payouts/quote" \
-H "Authorization: Bearer $CXC_KEY" \
-H "Content-Type: application/json" \
-d '{
"source_account_id": "'"$USDC"'",
"source_network": "solana",
"amount": "5.00",
"currency": "USDC",
"destination": {"type": "onchain", "trusted_beneficiary_id": "'"$TRUSTED"'"}
}')
echo "$QUOTE" | jq '{amount, currency, network, destination_network, minimum, valid, notes, quote_id, expires_at}'
QUOTE_ID=$(echo "$QUOTE" | jq -r '.quote_id')
valid: true means our checks passed; minimum is the provider's minimum in
the asset (1 USDC on chain). See Quotes and the route lock.
Step 7 — Send it
PAYOUT=$(curl -s -X POST "$CXC_BASE/v1/banking/payouts" \
-H "Authorization: Bearer $CXC_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: da-first-send-$RUN" \
-d '{
"source_account_id": "'"$USDC"'",
"source_network": "solana",
"amount": "5.00",
"currency": "USDC",
"destination": {"type": "onchain", "trusted_beneficiary_id": "'"$TRUSTED"'"},
"quote_id": "'"$QUOTE_ID"'"
}')
echo "$PAYOUT" | jq '{id, status, kind, network, destination_network, amount, currency}'
PAYOUT_ID=$(echo "$PAYOUT" | jq -r '.id')
202 Accepted with status: "submitted" and kind: "onchain". An API key
needs no step-up for a send to a trusted beneficiary. The send is fixed at
the source: amount is what leaves the wallet, at most two decimal places.
Step 8 — Follow it to settled
curl -s "$CXC_BASE/v1/banking/payouts/$PAYOUT_ID" \
-H "Authorization: Bearer $CXC_KEY" | jq '{status, tx_hash, settled_at}'
Poll until the status is final. There is no webhook for a payout's status.
The sandbox reads a send's status from the provider every 15 seconds for its
first ten minutes, so poll at about that pace. An on-chain send is completed
by the provider's sandbox — in our runs from about 25 seconds to a little
over a minute after it accepted the send, and in one run between about
45 seconds and 3½ minutes — and reads settled, with its tx_hash, at the
next of those reads. Statuses: Payouts. Sends the
provider's sandbox leaves in flight — to an IBAN or a bank account,
send-backs, swaps — the sandbox settles itself, by default from a minute
after the provider accepts it (five minutes for a us_bank send), once the money has
been seen leaving the wallet, and marks them simulated: true — see
when a send reads settled.
Next
- API transfer guide — the same in depth, and bank to wallet, with the timings and the errors a client meets.
- Sending — every destination, with the fields and a request for each: on chain and across chains, IBAN, US/UK/MX/BR bank accounts, @handle, swaps, send-back and convert.
- Accounts and wallets — EURC and the other
assets, wallets on more chains,
digital_actions. - Errors — every digital-asset error code.
- Sandbox behaviour — which sends finish in the sandbox, and which the sandbox settles itself.