Skip to content

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…
  1. POST /auth/nonces returns a single-use nonce and its expires_at.
  2. The wallet signs an EIP-4361 message that embeds the nonce. Its statement must be exactly Sign in to RAIL0, and its domain must be one the gateway allows.
  3. POST /auth with { 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.example
Version: 1
Chain ID: 84532
Nonce: 32891756
Issued At: 2026-10-01T12:00:00Z

Any library that builds SIWE messages works — siwe or viem/siwe in TypeScript, the siwe gem in Ruby, siwe-go in Go.

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.

A merchant account is born only when its wallet signs for it:

  1. POST /signup with { name, email, address } and no token. The answer is always 202 { status: "check_your_email" }, so it never reveals which emails are registered; a taken address or name is an explicit 409. 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.)
  2. POST /claim with { token, message, signature, terms_version }: token comes from the emailed link, and message is a SIWE message signed by the same wallet, with the statement Claim this RAIL0 merchant account with this wallet (nonce from POST /auth/nonces, as for login). It creates the account and its wallet and answers 201 with a session, exactly like POST /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.

RouteRequires
Blockchains & tokens, payment methods, health, sign-innothing
GET /payments, GET /disputesany session — results are filtered to payments where the address is payer or payee
GET /payments/{id} and its transactions / disputesa 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_disputethe payer’s session
releaseeither 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.

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.

  • POST /auth/logout revokes the presented token. If it answers revoked: false the revocation store was unreachable: treat your copy of the token as compromised until it expires.
  • POST /auth/revoke_all revokes every session of an address. It takes a fresh SIWE proof with the statement Sign out of RAIL0 everywhere instead 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.