Skip to main content

API keys and trusted beneficiaries

Sandbox only. Every digital-asset action works with an API key — onboarding, accounts, wallets, quotes, test funds, swaps, send-back and convert. A send to a third party is the exception: an API key sends only to a trusted beneficiary — a destination a person of your account approved, with their second factor. A key names the entry, never the destination: every detail of where the money goes is read from the entry.

A person's sessionAn API key
Send on chain, to an IBAN, to a bank account, by @handleInline or to a trusted beneficiary, with a step-upA trusted beneficiary only, no step-up
Approve or revoke a trusted beneficiaryYes, with a step-up; the seat needs payouts.createNever — 403 session_required, master keys included
List trusted beneficiariesYesaccounts:read
Swap, send back, convertWith a step-uppayments:write, no step-up

Why. A leaked key is the threat: whoever holds it can do everything its scopes allow. Because a key can only pay destinations a person already approved — after the cooldown, within the caps — the worst a leaked key does is pay those. Revoking the key or the entry stops it.

digital_actions on each digital-asset account says this deployment's rule: trusted_beneficiaries_required_for_api_keys: true and trusted_beneficiary_cooldown_seconds.


Approve one in the web app​

  1. Sign in at app.sandbox.cashxchain.com.
  2. On the Digital Assets page, on the Approved recipients card, choose Manage, then Approve a recipient.
  3. Give it a name, enter the destination and who holds it, and confirm with your second factor. An address of one of your own wallets is refused (422 destination_is_own_wallet).

Then read its id with your key:

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

The cooldown​

How long after an entry is approved before an API key may send to it: the time a person has to notice an approval they did not make. Sessions are not subject to it.

Cooldown
Sandbox0 — a key may use an entry at once
Production24 hours (no production sends yet)

While it runs, the entry's status is cooling_down and an API key's send is 422 beneficiary_not_trusted with details.reason: "cooling_down" and details.api_keys_usable_from.


GET /v1/banking/digital-assets/trusted-beneficiaries​

Your entries, newest first, revoked ones included. A session or a key with accounts:read.

JSON
{
"trusted_beneficiaries": [
{
"id": "6a0f5a8e-0c55-4d3f-9a43-2f7c7b2f0e11",
"kind": "onchain",
"label": "Muster Handels — treasury",
"network": "solana",
"address": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"beneficiary": {
"is_self": false, "type": "business", "name": "Muster Handels GmbH",
"address": { "street_line_1": "Hauptstr. 1", "city": "Berlin", "postal_code": "10115", "country": "DE" },
"business_identifier": { "type": "lei", "value": "506700GE1G29325QX363", "issuing_country": null },
"wallet_type": "external"
},
"created_by": "user:usr_2b…",
"created_at": "2026-11-03T09:00:00Z",
"revoked_at": null,
"revoked_by": null,
"status": "active",
"api_keys_usable_from": "2026-11-03T09:00:00Z"
}
]
}
status
activeA person or an API key may send to it
cooling_downA person may; an API key from api_keys_usable_from
revokedNobody may

Fields of other kinds are absent. An IBAN entry answers iban, bic and owner; a bank-account entry its coordinates in bank_account (a US entry's bank_name with them) and owner; a handle entry handle.


POST /v1/banking/digital-assets/trusted-beneficiaries​

A person of the account only, in their own session, with a step-up; the seat needs payouts.create. Idempotency-Key required. The body takes what the send it stands for takes, checked by the same rules — kind (the destination.type a send to it uses), a label (1 to 100 printable characters), and that kind's fields, flat:

kindFields
onchainnetwork, address, memo (optional, Stellar only), beneficiary — as an on-chain send
ibaniban, bic (optional), owner — as a send to an IBAN
us_bankrouting_number, account_number, account_type, bank_name (required), owner with a whole US address — as a US send
gb_banksort_code, account_number, owner
clabeclabe, owner
pixpix_key, document_number (optional), owner
cxc_handlehandle and nothing else
JSON
{ "kind": "onchain", "label": "Muster Handels — treasury",
"network": "solana", "address": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
"beneficiary": { "is_self": false, "type": "business", "name": "Muster Handels GmbH",
"address": { "street_line_1": "Hauptstr. 1", "city": "Berlin", "postal_code": "10115", "country": "DE" },
"business_identifier": { "type": "lei", "value": "506700GE1G29325QX363" },
"wallet_type": "external" } }
JSON
{ "kind": "iban", "label": "Erika — rent",
"iban": "DE89 3704 0044 0532 0130 00", "bic": "COBADEFFXXX",
"owner": { "type": "individual", "first_name": "Erika", "last_name": "Mustermann",
"address": { "street_line_1": "Domkloster 4", "city": "Köln", "postal_code": "50667", "country": "DE" } } }
JSON
{ "kind": "us_bank", "label": "Acme — ACH",
"routing_number": "101019644", "account_number": "123456789", "account_type": "checking",
"bank_name": "Lead Bank",
"owner": { "type": "business", "business_name": "Acme Supplies Inc",
"business_identifier": { "type": "lei", "value": "506700GE1G29325QX363" },
"address": { "street_line_1": "923 Folsom Street", "city": "San Francisco",
"postal_code": "94107", "state": "CA", "country": "US" } } }
JSON
{ "kind": "gb_bank", "label": "Maria — London",
"sort_code": "123456", "account_number": "34567890",
"owner": { "type": "individual", "first_name": "Maria", "last_name": "Silva",
"address": { "street_line_1": "10 Downing Street", "city": "London", "country": "GB" } } }
JSON
{ "kind": "clabe", "label": "Maria — SPEI",
"clabe": "032180000118359719",
"owner": { "type": "individual", "first_name": "Maria", "last_name": "Silva",
"address": { "street_line_1": "Av. Reforma 222", "city": "Ciudad de Mexico", "country": "MX" } } }
JSON
{ "kind": "pix", "label": "Maria — Pix",
"pix_key": "[email protected]", "document_number": "52998224725",
"owner": { "type": "individual", "first_name": "Maria", "last_name": "Silva",
"address": { "street_line_1": "Av. Paulista 1000", "city": "Sao Paulo", "country": "BR" } } }
JSON
{ "kind": "cxc_handle", "label": "Partner GmbH", "handle": "@their_handle" }
  • A field of another kind is 400; so is a bank_name on any kind but us_bank.
  • One of your own wallets' addresses is 422 destination_is_own_wallet.
  • A destination has at most one active entry: approving it again is 409 trusted_beneficiary_exists with details.trusted_beneficiary_id (revoke it first). A US account under another bank name is the same entry.
  • GBP, MXN and BRL for a business in the EEA are refused at approval: 422 corridor_unsupported, details.corridor {"type": "bank", "destination_type", "destination_currency"}. Nothing is approved and the step-up is not spent.
  • A cxc_handle entry approves the customer the handle names now. If the handle later names someone else, a send to the entry is refused (422 beneficiary_not_trusted, reason: "recipient_changed").
  • The same Idempotency-Key and body answer 200 with the entry (no step-up needed); another body under the key is 409 idempotency_key_reuse.

The step-up's binding: amount 0, currency *, and recipient trusted: followed by the destination in the form its send binds — trusted:solana:{address} (trusted:stellar:{address}:{memo} with a memo), trusted:{IBAN} (no spaces, upper case), trusted:us:{routing}:{account}:{checking|savings}, trusted:gb:{sort code}:{account}, trusted:clabe:{clabe}, trusted:pix:{key}[|{document}], trusted:@{handle}. See Step-up.

Answer: 201 with the entry, as in the list. The step-up is spent only after every other check has passed.

StatuserrorWhen
400bad_requestA malformed field (an address not on the chain, an IBAN, BIC, country, business identifier, a blank memo or one over 64 characters), a field of another kind, a missing network, address or iban, a us_bank without bank_name, a label or holder's name out of bounds; no Idempotency-Key
401two_factor_requiredNo step-up
403session_requiredAny API key, master keys included
403delegation_not_supportedA session acting for another account (Cxc-Account)
403permission_denied, step_up_binding_mismatch, step_up_not_linked, step_up_invalid, step_up_expired, step_up_already_usedA seat without payouts.create; the step-up
403account_not_verifiedYour business is not verified
409trusted_beneficiary_exists, idempotency_key_reuseAbove
422travel_rule_incomplete (details.fields: beneficiary.…, owner.…), destination_is_own_wallet, network_unsupported, corridor_unsupportedAbove
503digital_assets_unavailableNo digital-asset rail on this deployment

A us_bank entry approved before bank_name was required answers no bank_name and cannot be paid: its send, and a quote of it, is 400 bad_request with details.fields: ["bank_name"]. Revoke it and approve it again with one.


DELETE /v1/banking/digital-assets/trusted-beneficiaries/{id}​

The same callers, with a step-up bound to amount 0, currency *, recipient untrusted:{id}. Answer: 200 with the entry, status: "revoked", revoked_at and revoked_by. Revoking is final — the entry stays and keeps naming the payouts made to it, and nobody sends to it again; approve the destination anew for a new entry. Revoking a revoked entry answers it as it stands, with no step-up. 404 for an id that is not yours.

A send whose hold on the wallet was written before the revocation committed can still complete. To stop a key's sends at once, revoke the key as well.


Sending with an API key​

POST /v1/banking/payouts (and its quote) with destination: {"type": "<the entry's kind>", "trusted_beneficiary_id": "…"} and nothing else in destination. amount, currency, source_network, reference (to an IBAN or bank account) — and for a US account rail — are yours, as in any send. No step-up.

Bash
# On chain — the entry's chain is where the money goes; source_network is the
# wallet it leaves (the same chain, or another for a cross-chain send).
curl -s -X POST "$CXC_BASE/v1/banking/payouts" \
-H "Authorization: Bearer $CXC_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: key-onchain-$RUN" \
-d '{"source_account_id": "'"$USDC"'", "source_network": "solana",
"amount": "5.00", "currency": "USDC",
"destination": {"type": "onchain", "trusted_beneficiary_id": "'"$TRUSTED_ONCHAIN"'"}}'

# To an IBAN
curl -s -X POST "$CXC_BASE/v1/banking/payouts" \
-H "Authorization: Bearer $CXC_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: key-iban-$RUN" \
-d '{"source_account_id": "'"$EURC"'", "source_network": "solana",
"amount": "10.00", "currency": "EURC", "reference": "Invoice 4711",
"destination": {"type": "iban", "trusted_beneficiary_id": "'"$TRUSTED_IBAN"'"}}'

# To a US account, by wire
curl -s -X POST "$CXC_BASE/v1/banking/payouts" \
-H "Authorization: Bearer $CXC_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: key-us-$RUN" \
-d '{"source_account_id": "'"$USDC"'", "source_network": "solana",
"amount": "5.00", "currency": "USDC",
"destination": {"type": "us_bank", "trusted_beneficiary_id": "'"$TRUSTED_US"'", "rail": "wire"}}'

# By @handle
curl -s -X POST "$CXC_BASE/v1/banking/payouts" \
-H "Authorization: Bearer $CXC_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: key-handle-$RUN" \
-d '{"source_account_id": "'"$USDC"'", "source_network": "solana",
"amount": "5.00", "currency": "USDC",
"destination": {"type": "cxc_handle", "trusted_beneficiary_id": "'"$TRUSTED_HANDLE"'"}}'

gb_bank, clabe and pix entries are sent the same way, with their type.

  • The entry's destination and travel-rule data are checked again exactly as an inline destination is, and the send is checked against the entry once more when its hold is written: an entry revoked by then is refused (422 destination_not_payable, reason: "not_trusted", recorded failed, nothing sent).
  • Everything else is a send's: the caps, holds, minimums, replays, earlier_payout_unconfirmed, payout_outcome_unknown. A replay answers even after the entry is revoked.
  • A session may send the same body; its step-up is bound to where the money goes — the entry's own destination (solana:{address}, its IBAN, …) — not to the entry's id.
StatuserrorWhen
400trusted_beneficiary_requiredFrom an API key: an address, network, memo, beneficiary, iban, bic, owner, a bank account's fields or bank_name in destination, or no trusted_beneficiary_id. Nothing read, nothing recorded
400bad_requestThe entry is of another kind (type must be its kind); a trusted_beneficiary_id beside inline details
403insufficient_scopeA restricted key without payments:write (details.required_scope)
403session_requiredA key or session acting for another account (Cxc-Account)
404not_foundNo entry of yours has this id
422beneficiary_not_trusteddetails.reason: revoked (details.revoked_at), cooling_down (details.api_keys_usable_from), or recipient_changed (a handle entry whose handle names someone else now); details.trusted_beneficiary_id

Scopes, for reference​

A restricted key (cxc_rk_) needs the scope named; a secret key (cxc_sk_) and a master key hold every scope. Publishable keys reach none of these.

ActionRouteAPI key
Onboarding status, startGET / POST /v1/banking/kyb/digital-assetsaccounts:read / accounts:write
Sandbox approvalPOST /v1/sandbox/banking/kyb/digital-assets/approveaccounts:write
Open an account, a walletPOST /v1/banking/accounts, POST /v1/banking/digital-assets/walletsaccounts:write
Accounts, balances, wallets, capabilities, trusted beneficiariesGETaccounts:read
Credits, payouts, quotesGET /v1/transactions, GET /v1/banking/payouts, POST /v1/banking/payouts/quotepayments:read
Every send, swap, send-back, convertPOST /v1/banking/payoutspayments:write
Test funds, fiat test funds, a send-back's arrival, a send's completionPOST /v1/sandbox/banking/accounts/{id}/simulate-deposit, POST /v1/sandbox/banking/faucet, POST /v1/sandbox/banking/payouts/{id}/simulate-arrival, POST /v1/sandbox/banking/payouts/{id}/simulate-completionpayments:write
Approve, revoke a trusted beneficiaryPOST, DELETE …/trusted-beneficiariesnever

Two hops through your own bank account. A key with payments:write may send a digital balance back to your own EUR account (no step-up), and — with beneficiaries:write — register a bank beneficiary and pay it. Together a leaked key with both scopes reaches a destination of its own choosing in two hops. Give an integration's restricted key the narrowest scopes it needs: no beneficiaries:write beside payments:write unless it must register payees.