Step-up for sessions
A person moving money from a session — the web app, or anything acting
with a person's sign-in — confirms each payment with a second factor. The
proof is an X-Step-Up-Token, bound to the payment's amount,
currency and recipient: a token minted for one payment is refused for
any other. API keys never step up; see
API keys and trusted beneficiaries for what they
may do instead.
The web app does all of this for you. This page is for integrators who build their own interface on a person's session.
The binding, per send
| Send | amount | currency | recipient |
|---|---|---|---|
| On chain | amount | the asset | {network}:{address} — the chain the address is on, so across chains the destination's — or {network}:{address}:{memo} with a memo. solana:9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM |
| To an IBAN | amount | the asset | The IBAN without spaces, upper case: DE89370400440532013000 |
us_bank | amount | the asset | us:{routing}:{account}:{checking|savings} — us:101019644:123456789:checking |
gb_bank | amount | the asset | gb:{sort code}:{account}, digits only — gb:123456:34567890 |
clabe | amount | the asset | clabe:{clabe} |
pix | amount | the asset | pix:{key}, or pix:{key}|{document} with a document number; an email or random key lower case |
| By @handle | amount | the asset | @{handle}, lower case |
| Swap | amount | the asset sold | The destination_account_id |
| Send back to your EUR account | amount | EUR | The destination_account_id |
| Convert EUR into USDC | amount | EUR | The destination_account_id |
| To a trusted beneficiary | amount | the asset | The entry's own destination, in the form above — not its id |
| Approve a trusted beneficiary | 0 | * | trusted: and the destination in the form above: trusted:solana:{address}, trusted:{IBAN}, trusted:us:…, trusted:@{handle} |
| Revoke a trusted beneficiary | 0 | * | untrusted:{id} |
The rail of a US account and its bank_name are not part of the binding. A
replay of a send (the same Idempotency-Key and body) needs no step-up.
Normalisation. The binding compares the amount to two decimal places and the currency and recipient trimmed and lower-cased. So a token bound to a base58 address (Solana, Tron) or a memo also binds the same characters in another case — which is a different address. The send always goes to the address in the body, verbatim: bind the token to exactly what you send.
Minting a token
The auth portal mints it, after the person confirms with their authenticator code (or passkey), for the signed-in person:
POST https://auth.sandbox.cashxchain.com/api/auth/step-up
Content-Type: application/json
{
"code": "123456",
"txn": { "amount": "5.00", "currency": "USDC",
"recipient": "solana:9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM" }
}
{ "token": "eyJ…", "expires_in": 300 }
Send token as X-Step-Up-Token on the payout. It is single-use and valid
for five minutes. Two-factor authentication must be set up on the person's
sign-in first.
Refusals
| Status | error | When | What to do |
|---|---|---|---|
401 | two_factor_required | No X-Step-Up-Token | Mint one for this payment |
401 | step_up_expired | Older than five minutes | Mint a new one |
403 | step_up_invalid | Wrong signature, purpose, factor or person | Mint a new one |
403 | step_up_not_linked | Not bound to a payment | Mint one with txn |
403 | step_up_binding_mismatch | Bound to another amount, currency or recipient | Mint one for exactly this payment |
403 | step_up_already_used | Tokens are single-use | Mint a new one |
A refusal without details.idempotency_key_spent: true spent neither the
token nor the Idempotency-Key: correct the request and send it again. With
it, the send was recorded and the token spent — see
Errors.
Who may. A person of the paying account, in their own session, whose seat
holds payouts.create. A session acting for another account (Cxc-Account)
is refused 403 session_required on every send to a third party and on a
send-back — its step-up would be its own person's, not the customer's.