API Gateway
Authentication
The gateway has no API keys. A caller proves control of a wallet with Sign-In With Ethereum and gets a short-lived JWT, sent on every protected request:
Authorization: Bearer eyJhbGciOi…Signing in
Section titled “Signing in”POST /auth/noncesreturns a single-usenonceand itsexpires_at.- The wallet signs an EIP-4361 message that
embeds the nonce. Its
statementmust be exactlySign in to RAIL0, and its domain must be one the gateway allows. POST /authwith{ message, signature }verifies both, consumes the nonce, and returns{ token, address, account_id, name, expires_at }.
The message’s Chain ID must be 1 (what the SDKs sign with) or the id of any chain the
gateway currently serves, so a wallet that binds the message to its connected network
signs in too. A nonce is valid for 5 minutes, a session for 24 hours. Each session is
bound to the gateway environment that issued it: a staging token is refused in production
(401), and the other way round.
acme.example wants you to sign in with your Ethereum account:0x8ba1f109551bD432803012645Ac136ddd64DBA72
Sign in to RAIL0
URI: https://acme.exampleVersion: 1Chain ID: 84532Nonce: 32891756Issued At: 2026-10-01T12:00:00ZAny library that builds SIWE messages works — siwe or
viem/siwe in TypeScript, the
siwe gem in Ruby, siwe-go in Go.
Accounts are optional
Section titled “Accounts are optional”SIWE proves the address, so every wallet gets a session. When the address belongs to a
merchant account the token carries its account_id; otherwise account_id is null — an
account-less session, which is how buyers sign in to create, sign and list their own
payments.
Creating a merchant account
Section titled “Creating a merchant account”A merchant account is born only when its wallet signs for it:
POST /signupwith{ name, email, address }and no token. The answer is always202 { status: "check_your_email" }, so it never reveals which emails are registered; a taken address or name is an explicit409. The gateway emails a claim link, valid for 24 hours. (An operator inviting a merchant calls the same endpoint with an admin session; that link lasts 7 days.)POST /claimwith{ token, message, signature, terms_version }:tokencomes from the emailed link, andmessageis a SIWE message signed by the same wallet, with the statementClaim this RAIL0 merchant account with this wallet(nonce fromPOST /auth/nonces, as for login). It creates the account and its wallet and answers201with a session, exactly likePOST /auth. The account is operational at once.
A claim link from another environment, an expired one, or one replaced by a newer email
answers 422 (onboarding_link_invalid, onboarding_link_expired,
onboarding_link_superseded); a name, email or address taken in the meantime is a 409.
What a session can reach
Section titled “What a session can reach”| Route | Requires |
|---|---|
| Blockchains & tokens, payment methods, health, sign-in | nothing |
GET /payments, GET /disputes | any session — results are filtered to payments where the address is payer or payee |
GET /payments/{id} and its transactions / disputes | a session that is the payment’s payer or payee — anyone else gets 404, never 403 |
Merchant operations (authorize, charge, capture, void, refund) | the payee’s session |
dispute, close_dispute | the payer’s session |
release | either participant (only after authorization_expiry) |
/accounts/*, /webhooks, /analytics/* | a session carrying an account — 403 for an account-less one |
A merchant session acts through the one wallet it signed in with: it may change that
wallet’s label, accepted tokens and active flag, while a write on another wallet of the
same account is 403 not_your_wallet (reads stay account-wide). Adding wallets to an
account is suspended for now: POST /accounts/{account_id}/wallets answers
403 wallet_add_unavailable.
Failures
Section titled “Failures”A failed sign-in is a 422 whose code names the step: invalid_siwe, invalid_nonce,
nonce_used, signer_mismatch, siwe_purpose_mismatch, or one of the binding failures
siwe_domain_not_allowed, siwe_uri_mismatch, siwe_chain_mismatch,
siwe_proof_expired. siwe_domain_not_allowed is the one to surface in your UI: the origin
that built the message is not allowed by this gateway — a configuration problem, not a bad
signature.
Ending sessions
Section titled “Ending sessions”POST /auth/logoutrevokes the presented token. If it answersrevoked: falsethe revocation store was unreachable: treat your copy of the token as compromised until it expires.POST /auth/revoke_allrevokes every session of an address. It takes a fresh SIWE proof with the statementSign out of RAIL0 everywhereinstead of a token — the tool for a leaked token, when you hold the wallet but not the stolen session.
Deactivating a wallet (DELETE /accounts/{account_id}/wallets/{id}) takes effect at once:
the wallet may still sign in and read, but every write answers 403 wallet_deactivated.
The one exception is switching it back on (PATCH with { "active": true }), and only
when its owner switched it off: a wallet the rail0 operator deactivated answers
403 wallet_deactivated_by_operator, and the wallet’s deactivated_by says which.