Skip to main content

Stablecoin transfers with an API key

This guide is for an integration that moves stablecoins with an API key. It covers two flows, each as the calls you make, in order:

Sandbox only

Stablecoin sends to third-party wallets exist only in the sandbox today, at https://api.sandbox.cashxchain.com. Production offers no stablecoin send of any kind, and no digital-asset account or wallet: there, onboarding answers 503 digital_assets_unavailable, opening a USDC account 422 currency_unavailable, a send 422 corridor_unsupported, and the sandbox routes 404. In the sandbox nothing real moves, on any chain or bank.

What a send is. Your digital-asset accounts and their wallets are held at our crypto-asset service provider; CashXChain holds no private keys. A send is an instruction: CashXChain asks the provider to pay the amount out of your wallet's balance to the destination, and records what happens. A send on chain is requested with the chain of the wallet it leaves (source_network); the payout reads that chain as network, and the chain it is paid to as destination_network. The API never names an address the transfer is sent from.


Before you start​

You needWhere
A sandbox accountauth.sandbox.cashxchain.comSeparate from production: its keys and data are unknown there
A person with two-factor authenticationThat person's sign-in: an authenticator app or a passkey, set up in the web app's security settingsEvery recipient an API key pays is approved by a person with their second factor. The sandbox has no bypass
A secret keydev.sandbox.cashxchain.com → API keys → Create keyCreated by a person whose seat holds api_keys.manage (an owner or admin). The token is shown once. A restricted key (cxc_rk_) works too, with accounts:read, accounts:write, payments:read and payments:write. See Managing API keys
curl and jqYour shellThe examples below use both

Plans. In the sandbox the API is open on every plan, the Free plan included: secret keys, webhooks and every route on this page. A plan's limits still apply; for this guide, its number of currencies — all digital assets together count as one — and its payment limits on a convert and on a send-back to your EUR account, measured on the EUR (422 plan_limit_exceeded, see Plan limits). Stablecoin sends do not count against those; they have caps of their own, per asset. Seats count in the sandbox too: on a one-seat plan, the person who creates the key is the one who approves its recipients.

The web app. A person approves recipients on the Digital Assets page, app.sandbox.cashxchain.com/digital-assets. On the Free plan the navigation shows that entry greyed, labelled Starter, and opens an upgrade dialog; the page itself opens at its address on every plan, and approving a recipient there needs no upgrade.

Verification. In the sandbox a business verification is approved when it is referred, and an account still pending may already do everything here — see KYB. Your business's country, the one your account was opened with, decides some routes: a business in the EEA is offered no GBP, MXN or BRL payout and no USDT across chains (Regions).

Bash
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

Wallet to wallet​

1. The key and the Idempotency-Key​

  • Authorization: Bearer $CXC_KEY on every request. A key needs no step-up, and sends to a third party only to a trusted beneficiary.
  • Idempotency-Key on every write below but the quote, the sandbox approval, simulate-completion and the webhook registration, which take none. Take it from your own record of the operation, and keep it across retries. What a repeat does depends on the route:
    • a payout or the faucet: the same key and body answer what the first request did — a payout 200, as it stands now — and another body under the key is 409 idempotency_key_reuse;
    • opening an account: a repeat is 409 conflict (you already hold it), and another currency under the same key opens that currency;
    • opening a wallet: the key is not used — the wallet is yours on that chain, 201 the first time and 200 after;
    • test funds: another amount or chain under the same key is a new deposit (Test funds).
  • After a refusal, look for details.idempotency_key_spent. Without it, nothing was recorded: correct the request and send it again under the same key. With it, the send was recorded and is failed (or, after 503 payout_outcome_unknown, being confirmed), and a new send takes a new key — see Was anything recorded?

See Idempotency and Authentication.

2. Capabilities​

Bash
curl -s "$CXC_BASE/v1/banking/capabilities" \
-H "Authorization: Bearer $CXC_KEY" \
| jq '{environment, flows: [.flows[] | {flow, status, reason}], digital_rules, route_lock, account}'
Read
environmentsandbox
flowsstablecoin_to_stablecoin and cross_chain read sandbox_only — or requires_onboarding, with the step in reason, until step 3 is done
digital_rulesapi_keys_send_only_to_trusted_beneficiaries: true, and trusted_beneficiary_cooldown_seconds — 0 by default in the sandbox
route_lockttl_seconds — how long a quote's route lock holds (900 unless the deployment sets otherwise)
digital_assetsPer asset offered: the chains it is sent from and to, for your business's region, and its caps per send, day and month (limits)
accountverified (may move money), digital_asset_account (holds an open USDC account), digital_assets_held
actionsEvery banking route, and whether the route policy admits this key (allowed_by_policy)

Capabilities say what is offered, not that a given request will be accepted. Every field: Capabilities.

3. A USDC account and its wallets​

Approve your business at the provider's sandbox, then open the account. The approval takes no body and no Idempotency-Key, and calling it again is harmless (at most 30 calls an hour). Unless you completed a hosted verification, the approval makes and approves a test customer at the provider, and in the sandbox that test customer — not your business — is the originator the provider records on your sends (Onboarding).

Bash
curl -s -X POST "$CXC_BASE/v1/sandbox/banking/kyb/digital-assets/approve" \
-H "Authorization: Bearer $CXC_KEY" | jq '{state, can_open_account, rails}'

curl -s -X POST "$CXC_BASE/v1/banking/accounts" \
-H "Authorization: Bearer $CXC_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 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"
  • 201 with the account, which opens with a wallet on Solana. 409 conflict: you already hold one — carry on with its id. 409 digital_assets_onboarding_required: the approval has not gone through.
  • One account per asset; EURC and the others open the same way. All of them together count as one currency of your plan.
  • In GET /v1/banking/accounts the account lists its identifiers — the EUR IBAN first, then one onchain identifier (network, asset, address) per wallet — and digital_actions: the chains it sends from (send_onchain) and the chains an address may be on (send_onchain_to). See Accounts and wallets.

A wallet per chain, shared by your asset accounts:

Bash
curl -s "$CXC_BASE/v1/banking/digital-assets/wallets" \
-H "Authorization: Bearer $CXC_KEY" \
| jq '{wallets: [.wallets[] | {network, address, balances}], networks}'

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

201 with the new wallet, or 200 with the one you already hold on that chain. The chains and the assets on each are in the overview.

4. Test funds​

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: test-funds-$RUN" \
-d '{"amount": "100", "network": "solana"}' | jq

curl -s "$CXC_BASE/v1/banking/accounts/$USDC/balance" \
-H "Authorization: Bearer $CXC_KEY" | jq '.balances[] | {available, by_network}'

202 — {"account_id", "currency", "network", "amount", "status": "simulated"}. The deposit is booked usually within five seconds, at most five minutes: the balance, an onchain_transfer on GET /v1/transactions, and a payin.received webhook. At most 100,000 a request with six decimal places, and 30 requests an hour per account. Test funds are how a sandbox wallet is funded: no asset sent on chain to a sandbox address arrives. See Test funds.

5. A trusted beneficiary​

An API key pays a third party only to a trusted beneficiary: a destination a person of your account approved with their second factor. The key names the entry; where the money goes is read from it.

An API keyA person, in the web app
List the entriesGET /v1/banking/digital-assets/trusted-beneficiaries (accounts:read)Yes
Send to an entryYes, by its id, no step-upYes, with a step-up
Send to an address no one approvedNever — 400 trusted_beneficiary_requiredYes, with a step-up
Approve or revoke an entryNever — 403 session_required, master keys includedYes, with a step-up; the seat needs payouts.create

A person approves it:

  1. Sign in at app.sandbox.cashxchain.com and open Digital Assets.
  2. On the Approved recipients card choose Manage, then Approve a recipient.
  3. Give the recipient a name, and enter the chain, the address and who holds the wallet. A third party needs a name, street, city and country, a business also its identifier (LEI, VAT or company registration), and the wallet's type: at an exchange or another custodian, or self-custodied. Your own wallet elsewhere needs only that it is yours, and its type.
  4. Confirm with the second factor. The step-up is bound to that destination alone.

The address may not be one of your own wallets: that is refused 422 destination_is_own_wallet. To test, use a wallet address of another sandbox account, or any valid address on that chain — in the sandbox nothing is sent on chain.

Each destination is approved on its own. An entry is one chain and one address (with a memo on Stellar); it names no asset — a send to it gives its currency, as any send does. Its chain may be one of your wallets' chains or a chain the asset is sent to across chains (digital_actions.send_onchain_to). The fields, and the other kinds of entry: API keys and trusted beneficiaries.

The cooldown. How long after an approval before a key may use the entry: digital_rules.trusted_beneficiary_cooldown_seconds in capabilities. By default it is 0 in the sandbox, so an entry is usable at once, and at least 24 hours in production, which sends nothing to third parties. While it runs, the entry reads cooling_down and a key's send is 422 beneficiary_not_trusted with details.api_keys_usable_from.

Read the entry's id with the key:

Bash
curl -s "$CXC_BASE/v1/banking/digital-assets/trusted-beneficiaries" \
-H "Authorization: Bearer $CXC_KEY" \
| jq '.trusted_beneficiaries[] | {id, kind, label, network, address, status, api_keys_usable_from}'

TRUSTED=$(curl -s "$CXC_BASE/v1/banking/digital-assets/trusted-beneficiaries" \
-H "Authorization: Bearer $CXC_KEY" \
| jq -r '[.trusted_beneficiaries[] | select(.kind == "onchain" and .status == "active")][0].id')
echo "$TRUSTED"
  • Revoking is a person's, with a step-up, and final. A send whose hold on the wallet was written before the revocation can still complete: to stop a key's sends at once, revoke the key as well.
  • Your own self-custodied wallets may receive at most 1,000 of each asset (USDC, EURC) over a rolling 30 days, this send included: 422 wallet_attestation_required. Wallets of yours at a custodian, and third parties' wallets, are not counted.

6. Quote and lock the route​

A quote moves nothing and records no payout; it stores only the route lock that quote_id names. No Idempotency-Key, no step-up, key scope payments:read. From a key, destination carries type and trusted_beneficiary_id and nothing else.

Bash
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 '{network, destination_network, minimum, fees, network_fee, valid, notes, quote_id, expires_at, rate_locked, route}'
QUOTE_ID=$(echo "$QUOTE" | jq -r '.quote_id')
Field
network, destination_networkThe chain of the wallet it leaves, and the entry's chain
minimumThe provider's minimum for this send, in the asset: 1 USDC on chain
fees, network_feenull, not zero: the provider charges when the send executes, and no fee figure is returned for an on-chain send
valid, notesOur own checks' verdict; where the deployment runs the provider's dry run, the provider's. The API refuses no payout on valid: fix what a refusal names before you send
quote_id, expires_atThe route lock: single use, until expires_at — 15 minutes unless the deployment sets otherwise
routeWhat the payout will do: kind onchain, one step onchain_transfer of the asset on the entry's chain, by casp. No provider is named
rate_lockedAlways false: the lock holds the route, never a rate

See Quotes and the route lock.

7. Send​

Bash
PAYOUT=$(curl -s -X POST "$CXC_BASE/v1/banking/payouts" \
-H "Authorization: Bearer $CXC_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 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, quote_id}'
PAYOUT_ID=$(echo "$PAYOUT" | jq -r '.id')
  • 202 Accepted, status submitted, kind onchain. Key scope payments:write; no step-up.
  • amount is what leaves the wallet, exactly, with at most two decimal places; the payout reads it back at the asset's six ("5.000000").
  • The wallet must cover the amount plus its chain's headroom — 1 %, 5 % on Ethereum — less what your other sends from it hold (Holds).
  • Under quote_id the body must repeat what was quoted (422 route_quote_mismatch names what differs), and the payout runs on the quoted route or is refused (409 route_changed: nothing sent, quote again). Without quote_id it is routed as usual.
  • Keep quote_id and route from the 202: GET /v1/banking/payouts and GET /v1/banking/payouts/{payout_id} do not return them.
  • Across chains: an entry on another chain than source_network gets the same asset on its chain — never a swap — where the provider routes it (Across chains).

8. Follow it to settled​

There is no webhook for a payout's status: poll it.

Bash
for i in $(seq 1 40); do
STATUS=$(curl -s "$CXC_BASE/v1/banking/payouts/$PAYOUT_ID" \
-H "Authorization: Bearer $CXC_KEY" | jq -r '.status')
echo "$(date +%T) $STATUS"
case "$STATUS" in settled|failed|cancelled|returned|needs_reconciliation) break ;; esac
sleep 15
done
curl -s "$CXC_BASE/v1/banking/payouts/$PAYOUT_ID" \
-H "Authorization: Bearer $CXC_KEY" | jq '{status, tx_hash, settled_at, simulated}'
  • Statuses: submitted, processing, then settled — or failed, cancelled, returned. Two more can appear: created, while a send is being confirmed after 503 payout_outcome_unknown; and needs_reconciliation, when the provider reports a state only a person can resolve — support resolves it, and it then reads submitted, processing, settled, failed or cancelled. On chain, tx_hash appears once the provider reports the send completed. In the sandbox neither a tx_hash nor an address is on any public chain: neither can be looked up on an explorer. See the lifecycle.
  • Pace. The sandbox reads a send's status from the provider every 15 seconds for its first ten minutes — up to 10 payouts a read across the whole sandbox, so with more in flight each is read every few reads — and about every two minutes while it is in flight. Polling every 15 seconds keeps up; polling counts against your 600 requests a minute (Rate limits).
  • Lists. GET /v1/banking/payouts lists your payouts newest first, with has_more and next_cursor (sent back as before) — see Listing.
  • Webhooks. The only money event is payin.received, for each credit booked on your accounts — test funds, a convert's USDC, a send to you by @handle, a swap's bought asset. No event reports a payout. Register an endpoint with POST /v1/webhooks and {"url": "…", "events": ["payin.received"]}, with a secret key or a restricted key with accounts:write (Webhooks).

9. Sandbox timings​

The public sandbox receives no webhooks from the provider: these timings come from the sandbox's own reads, and when the provider's sandbox completes a send or takes its money, it does not say in advance.

WhatWhen, in the sandbox
Test fundsBooked usually within five seconds, at most five minutes
A send on chain, across chains or by @handleCompleted 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 settled at the next 15-second read of its status
A send to an IBAN, a gb_bank, clabe or pix account, a send-back, a swapLeft in flight by the provider's sandbox, and settled by ours (simulated: true) once the money has been seen leaving the wallet: with the default one-minute wait, 60 to 75 seconds after acceptance where the provider has listed its debit by about 45 seconds, otherwise within about 15 seconds of the first read of the wallet after it is listed
A send to a us_bank accountLeft to the provider's sandbox for five minutes; one it completes reads settled with no simulated. Still in flight then, it is settled by ours: by default 300 to 315 seconds after acceptance where its debit is listed by about 285 seconds
simulate-completion on one of the sends ours settlesAt once, whatever its wait, once its debit has been seen — before that, 409 not_simulatable (debit_not_seen): ask again in a few seconds
A convert from your sandbox EUR accountsettled in the 202; the USDC is booked within seconds

The reads behind them, and every case, are in When a send reads settled.

10. Errors​

Every error has one envelope: {"success": false, "error": "<code>", "message": "…", "details": {…}}. Branch on error and details. The ones this flow meets:

StatuserrorWhenWhat to do
401unauthorizedThe key is unknown or revoked, or from the other environmentCheck the key and CXC_BASE
403insufficient_scopeA restricted key without the route's scope (details.required_scope)Use a key that holds it
403session_requiredA key approving or revoking an entry; a key acting for another account (Cxc-Account) sending to a third partyA person approves entries in the web app; send with the account's own key
409digital_assets_onboarding_requiredOpening an account or a wallet, or sending, before the approval has gone through (on opening, details.state and details.next_steps)Step 3
403currency_limit_reachedThe plan's currencies are used upClose an account, or upgrade
409wallet_not_openNo wallet of yours on that chain (source_network, or a deposit's network)Open one, step 3
400trusted_beneficiary_requiredThe key sent an address, network, memo or beneficiary in destination, or no trusted_beneficiary_id. Nothing recordedSend the entry's id alone
400bad_requesttype is not the entry's kind; more than two decimal places; currency not the account's asset; no source_network; a reference on chainCorrect it, same key
404not_foundNo entry, account or quote of yours has that idCheck the id
422beneficiary_not_trusteddetails.reason: revoked, cooling_down (details.api_keys_usable_from)Revoked: a person approves the destination anew. Cooling down: wait until api_keys_usable_from
422amount_below_minimumUnder the provider's minimum (details.minimum): 1 of the asset on chainSend more
422asset_not_on_networkThe chain does not carry the asset hereAnother chain
422corridor_unsupportedNot a route offered here (details.corridor): sends off, production, a chain the asset is not sent to, USDT across chains for a business in the EEACheck digital_actions
422wallet_attestation_requiredPast 1,000 of the asset to your own self-custodied wallets in 30 days (details.sent, details.remaining)Wait for the window
422digital_send_limit_exceededOver a per-asset cap — by default in the sandbox 10,000 a send, 50,000 a day, 250,000 a month; a deployment's caps are digital_assets[].limits in capabilities (details.cap, used, remaining)Send less
422route_quote_expiredAfter expires_atQuote again
422route_quote_mismatchThe body is not the one quoted (details.fields)Send the quoted body
409route_quote_usedThe quote was spent on another payoutQuote again
409route_changedThe route would be another now (details.quoted_route, details.current_route). Nothing sentQuote again
409earlier_payout_unconfirmedAn earlier send of yours is still being confirmed (details.payout_id). Nothing recordedSame key, once that payout reads submitted or failed
409idempotency_key_reuseThe key was used for another bodyA new key for a new send
400insufficient_balanceRecorded. The wallet, less what your other sends hold, does not cover the amount and its headroom; the message names bothA new key, once enough is free
422destination_not_payableRecorded, failed, nothing held. details.reason provider_refused: the provider refused the transfer when it was created (details.provider_field names a field it refused); not_trusted: the entry was revoked before the send's hold was writtenChange what it names, then a new key; for a refusal in our words, contact support first
503payout_outcome_unknownRecorded. The provider did not confirm the sendSend it again only under the same key: it reads submitted once the provider is found to hold it, or failed after seven minutes or more
503service_unavailableThe provider is unavailable. Nothing recorded, unless details.idempotency_key_spentRetry with backoff, same key — or a new key if it was recorded
429rate_limitedOver 600 requests a minute for the key; a sandbox tool past 30 an hourWait Retry-After, or the minutes the message names

Every code, and every route's: Digital-asset errors and the error reference.


Bank to wallet​

The way from a bank account to a wallet is two legs, each its own request: EUR into your USDC account, where it is held as USDC in your own wallet; then a send from that wallet to a trusted beneficiary, as in steps 5 to 8. Have steps 1 to 3 done first.

1. The fiat pay-in details​

The USDC account has an EUR IBAN of its own. EUR paid to it by SEPA arrives as USDC in the account's wallet on Solana — the provider converts it at its rate when it arrives, less its fees — and is booked as a credit, with payin.received.

Bash
curl -s "$CXC_BASE/v1/banking/accounts/$USDC/payin-instructions" \
-H "Authorization: Bearer $CXC_KEY" \
| jq '.instructions[] | {rail, currency, account_holder_name, beneficiary_is_account_holder, notice, identifier}'
  • The SEPA instruction comes first: rail sepa, currency EUR, the IBAN and BIC in identifier, and account_holder_name — the IBAN's holder exactly as the provider states it. Where that is not your company's name, beneficiary_is_account_holder is false and notice tells the payer to enter the name as shown.
  • Then one on-chain instruction per wallet that carries the asset: rail the chain, currency USDC, identifier {"scheme": "onchain", "network", "asset", "address"}.
  • In the sandbox nothing reaches these details: no SEPA transfer and no on-chain transfer is credited, and EUR to the IBAN cannot be simulated — the provider's sandbox refuses it. Use step 2.

2. Money in, in the sandbox​

Two ways, depending on what you test:

  • The bank leg as well: fund your sandbox EUR account with the faucet, then convert it.
  • The wallet leg alone: test funds straight into the USDC wallet, as in step 4 above.
Bash
# Outside the euro area a business's first account is not EUR: open it first.
# It takes one of your plan's currencies; 409 conflict if you hold it already.
curl -s -X POST "$CXC_BASE/v1/banking/accounts" \
-H "Authorization: Bearer $CXC_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: open-eur-$RUN" \
-d '{"currency": "EUR"}' | jq '{currency_account, error, message}'

EUR=$(curl -s "$CXC_BASE/v1/banking/accounts" \
-H "Authorization: Bearer $CXC_KEY" \
| jq -r '.accounts[] | select(.currency == "EUR") | .id')
echo "$EUR"

curl -s -X POST "$CXC_BASE/v1/sandbox/banking/faucet" \
-H "Authorization: Bearer $CXC_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: faucet-eur-$RUN" \
-d '{"currency": "EUR", "amount": 100}' | jq

200 — {"currency": "EUR", "amount": "100", "balance": "…"}. amount is whole euros, 1 to 100,000; with an Idempotency-Key it is credited once. 409 when you hold no EUR account: open one with POST /v1/banking/accounts and {"currency": "EUR"}. At most 30 requests an hour. See Test money.

3. Convert EUR into USDC​

A payout from your EUR account to your USDC account. It is offered where the EUR account's own_account_payouts reads ["EUR"]. Where this deployment does not convert, the convert method of fiat_to_stablecoin in GET /v1/banking/capabilities reads not_offered, with its reason, and a convert answers 422 corridor_unsupported: fund the wallet with test funds instead.

Bash
curl -s "$CXC_BASE/v1/banking/accounts" -H "Authorization: Bearer $CXC_KEY" \
| jq '.accounts[] | select(.currency == "EUR") | {id, own_account_payouts}'

# The estimate: nothing moves
curl -s -X POST "$CXC_BASE/v1/banking/payouts/quote" \
-H "Authorization: Bearer $CXC_KEY" \
-H "Content-Type: application/json" \
-d '{"source_account_id": "'"$EUR"'", "destination_account_id": "'"$USDC"'",
"amount": "20.00", "currency": "EUR"}' \
| jq '{destination_amount_estimate, rate, rate_pair, rate_as_of, indicative, fees}'

# The convert
curl -s -X POST "$CXC_BASE/v1/banking/payouts" \
-H "Authorization: Bearer $CXC_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: convert-$RUN" \
-d '{"source_account_id": "'"$EUR"'", "destination_account_id": "'"$USDC"'",
"amount": "20.00", "currency": "EUR"}' \
| jq '{id, status, kind, amount, currency, destination_account_id}'
  • amount is the EUR that leaves, at least 1.00, two decimal places. What arrives is the provider's conversion at its rate, less its fees; the quote's destination_amount_estimate estimates it, in USDC, never locked.
  • No beneficiary_id, fx_quote_id or reference. A key needs no step-up.
  • In the sandbox the 202 already reads settled (kind convert). The estimated USDC is deposited into your Solana wallet and booked within seconds, with payin.received; the payout shows it as arrival within about ten minutes. Two converts within ten minutes whose estimates lie within about 5 % of each other cannot be told apart: space them out or size them differently. At most 30 converts an hour.
  • Plan limits apply, measured on the EUR: 422 plan_limit_exceeded. Too little EUR is 400 insufficient_balance.

Every case: Convert EUR into USDC.

4. Send to a trusted wallet​

The USDC is in your Solana wallet: quote and send it with "source_network": "solana" to a trusted beneficiary, and follow it, as in steps 5 to 8.

Bash
curl -s "$CXC_BASE/v1/banking/accounts/$USDC/balance" \
-H "Authorization: Bearer $CXC_KEY" | jq '.balances[] | {available, by_network}'

5. And on to a bank​

From the USDC account — and for some kinds an EURC account — a key also pays a bank account, in the sandbox: to a trusted beneficiary of that kind, which a person approves in the web app as in step 5 of wallet to wallet, or back to your own EUR account with no entry.

Destinationdestination.typeFromRegion
Your own EUR account (send-back)destination_account_id, no entryUSDCEvery
An IBAN, EUR over SEPAibanUSDC, EURCEvery
A US account, USD over ACH or wireus_bankUSDC, EURCEvery
A UK account, GBP over Faster Paymentsgb_bankUSDCOutside the EEA
A Mexican CLABE, MXN over SPEIclabeUSDC, EURCOutside the EEA
A Brazilian Pix key, BRL over PixpixUSDCOutside the EEA
  • The send-back is offered where the USDC account's own_account_payouts reads ["EUR"]; its amount is the EUR that arrives, and the account must be held in your company's legal name.
  • Each bank kind is paid over a rail the provider has enabled for you — rails in GET /v1/banking/kyb/digital-assets, which the sandbox approval requests and approves; check that the rail reads enabled. Otherwise 409 digital_assets_onboarding_required with details.rail.
  • A Pix payout needs a business outside the EEA, USDC, at least 2 USDC, and the BRL (Pix) rail. A business in the EEA is refused 422 corridor_unsupported, and so is the approval of a pix entry.
  • In the sandbox these payouts settle as in step 9.

Fields, minimums and examples: To a bank account outside SEPA and Send back to your EUR account.

What does not exist​

  • No on-ramp straight into an outside wallet. EUR paid to the IBAN always arrives as USDC in your own wallet at the provider, on Solana. Paying it on to another wallet is a second payout.
  • No BRL pay-in — no Pix in, nor any other BRL deposit — and no USD, GBP or MXN pay-in into a digital-asset account. The only fiat way in is EUR by SEPA to the USDC account's IBAN, and it arrives as USDC, never as EURC. Pix exists only as a payout from USDC.
  • No send from a wallet held elsewhere. Every send leaves one of your wallets at the provider. No route sends from an outside wallet, and no deposit address pays a bank account on its own.
  • No single request for the whole way. Each leg is its own payout, with its own Idempotency-Key and status.
  • No production. None of this is offered in production.