Skip to content

API reference

Wallets

Account-scoped wallet and wallet-token management.

List the account's wallets, each with its token holdings nested

GET /accounts/{account_id}/wallets

Bearer JWT (Sign-In With Ethereum)

Owner or admin.

Path parameters

  • account_id string (uuid) required

Query parameters

  • page integer

    1-based page number.

  • per_page integer

    Items per page (capped at 100).

  • sort string

    Comma-separated sort fields; prefix with - for descending (e.g. -created_at,status). An unsupported field is rejected with 422 invalid_sort, whose detail names the allowed set — it is never silently dropped.

  • chain_id integer

    Restrict nested tokens to this chain ID (does not hide wallets).

  • token_symbol string

    Restrict nested tokens to this symbol (does not hide wallets).

  • active boolean

    Filter wallets by active status.

  • default boolean

    Restrict nested token holdings to the default one (does not hide wallets).

  • token_active boolean

    Restrict nested token holdings to this active status (does not hide wallets).

Responses

  • 200 The account's wallets, each with its token holdings nested under tokens (empty when the wallet has none).
Terminal window
curl "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
[
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"label": "Main checkout wallet",
"active": false,
"deactivated_by": "owner",
"tokens": [
{
"token_id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"token": {
"chain_id": 84532,
"symbol": "USDC",
"address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"decimals": 6,
"active": false
},
"active": false,
"default": false
}
]
}
]

Add a wallet to the account (suspended)

POST /accounts/{account_id}/wallets

Bearer JWT (Sign-In With Ethereum)

SUSPENDED: every session is refused 403 wallet_add_unavailable ("Adding a wallet is not available yet."). A session is bound to the one wallet it signed in with; adding another wallet will return with a dedicated merchant wallet-management section that requires a fresh signature by the wallet being added. The request shape and the proof verification below are kept for that section. When it returns: Registers a wallet on the account. Owner only. Requires a SIWE proof-of-ownership of the address being added: obtain a single-use nonce from POST /auth/nonces, build an EIP-4361 message carrying that nonce and signed by the address's private key, and submit message + signature here. The gateway verifies the signature recovers to address, the nonce is unused/unexpired, and the message binds to an allowed SIWE domain/chain — otherwise 422. The proven address need not equal the session address (a merchant may control several payee wallets). An admin cannot add a wallet to another account (403): the proof is exactly what an operator cannot produce. Wallet addresses are globally unique: an address already registered (to this or any other account) yields 409.

Path parameters

  • account_id string (uuid) required

Request body

  • address string required

    EVM wallet address to add (0x, 40 hex). Must be the address that signed the SIWE message.

  • message string required

    EIP-4361 SIWE message text signed by the address being added (carries the nonce from POST /auth/nonces). Purpose-bound: its statement must be exactly "Add this wallet to your RAIL0 account" — a login-shaped proof is refused with 422 siwe_purpose_mismatch, so a login signature can never be replayed here to bind someone else's wallet to the caller's account.

  • signature string required

    Signature over the SIWE message (0x…), proving control of the address's private key.

  • label string

    Human-readable label.

Responses

  • 201 Wallet created (unreachable while the route is suspended).
  • 400 Validation error.
  • 401 Authentication failed.
  • 403 Caller not permitted.
  • 409 The address is already registered to an account (addresses are globally unique).
  • 422 The SIWE proof-of-ownership failed: unparseable message, unknown/used/expired nonce, domain/chain mismatch, or a signature that does not recover to address.
Terminal window
curl -X POST "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets" \
-H "Authorization: Bearer $RAIL0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"message": "acme.example wants you to sign in with your Ethereum account: …",
"signature": "0x6b1f…9c1b"
}'
Response 201
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"label": "Main checkout wallet",
"active": false,
"deactivated_by": "owner"
}

Get a single wallet (by id or address)

GET /accounts/{account_id}/wallets/{id}

Bearer JWT (Sign-In With Ethereum)

Path parameters

  • account_id string (uuid) required
  • id string required

    Wallet id (UUID) or 0x address. The address is unique per account, so either resolves to the same wallet.

Responses

  • 200 Wallet.
  • 404 Resource not found.
Terminal window
curl "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"label": "Main checkout wallet",
"active": false,
"deactivated_by": "owner"
}

Update a wallet label or active status

PATCH /accounts/{account_id}/wallets/{id}

Bearer JWT (Sign-In With Ethereum)

Owner or admin. An owner session may change ONLY the wallet it signed in with (the session's address): another wallet of the same account is refused 403 not_your_wallet. An admin may change any wallet, including the guarded switch-off. Last-active-wallet guard: active: false on the account's only active wallet is refused with 422 last_active_wallet. Every switch-off records who did it (deactivated_by: owner when the session is signed in with this wallet, operator when an admin session acts on it). Switching back on (active: true on an inactive wallet): an admin may always; the owner's session may switch its OWN wallet back on — this is the one write a deactivated session may make, anything else from it is 403 wallet_deactivated — unless an operator switched it off, or the switch-off predates the record (deactivated_by null), which is refused 403 wallet_deactivated_by_operator: an operator's switch-off revokes the key, so only an operator lifts it. Residual limit: a wallet its owner switched off can be switched back on by whoever holds its key; a definitive revocation must come from an operator.

Path parameters

  • account_id string (uuid) required
  • id string required

    Wallet id (UUID) or 0x address. The address is unique per account, so either resolves to the same wallet.

Request body

  • label string
  • active boolean

Responses

  • 200 Wallet updated.
  • 401 Authentication failed.
  • 403 not_your_wallet (another wallet of the session's account), wallet_deactivated (a deactivated session doing anything but switching its own wallet back on), wallet_deactivated_by_operator (the owner switching back on a wallet an operator switched off, or one whose switch-off predates the record), or account_deactivated.
  • 404 Resource not found.
  • 422 last_active_wallet: the wallet is the account's last active one.
Terminal window
curl -X PATCH "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b" \
-H "Authorization: Bearer $RAIL0_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'
Response 200
{
"id": "5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b",
"address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"label": "Main checkout wallet",
"active": false,
"deactivated_by": "owner"
}

Deactivate a wallet (soft delete)

DELETE /accounts/{account_id}/wallets/{id}

Bearer JWT (Sign-In With Ethereum)

Owner or admin. An owner session may change ONLY the wallet it signed in with (the session's address): another wallet of the same account is refused 403 not_your_wallet. An admin may switch off any wallet. Only ever deactivates (payments name the address, so the row stays; there is no removal). Last-active-wallet guard: the account's only active wallet is refused with 422 last_active_wallet. Records who switched it off (deactivated_by), which decides who may switch it back on (see PATCH): an operator's switch-off only an operator lifts, and an operator switching off a wallet its owner already switched off takes the switch-off over.

Path parameters

  • account_id string (uuid) required
  • id string required

    Wallet id (UUID) or 0x address. The address is unique per account, so either resolves to the same wallet.

Responses

  • 204 Wallet deactivated.
  • 401 Authentication failed.
  • 403 Caller not permitted.
  • 404 Resource not found.
  • 422 last_active_wallet: the wallet is the account's last active one.
Terminal window
curl -X DELETE "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 204 · No content

Get the wallet's on-chain balances (native + tokens)

GET /accounts/{account_id}/wallets/{id}/balances

Bearer JWT (Sign-In With Ethereum)

Reads the wallet address's on-chain balances — native gas token + active ERC-20 tokens — across all active chains, or one chain via chain_id. Protected: only the wallet's own account may read it. Chains are read in parallel; one whose RPC is unreachable is returned with an error instead of failing the whole response.

Path parameters

  • account_id string (uuid) required
  • id string required

    Wallet id (UUID) or 0x address.

Query parameters

  • chain_id integer

    Restrict to one chain; omit for all active chains.

  • token_symbol string

    Restrict tokens to this symbol; omit for all active tokens.

Responses

  • 200 The wallet's balances per chain.
  • 401 Authentication failed.
  • 403 Caller not permitted.
  • 404 Resource not found.
Terminal window
curl "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/balances" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
{
"wallet_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"balances": [
{
"chain_id": 84532,
"network_type": "string",
"native": {
"symbol": "USDC",
"address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"decimals": 6,
"raw": "string",
"amount": "10500000"
},
"tokens": [
{
"symbol": "USDC",
"address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"decimals": 6,
"raw": "string",
"amount": "10500000"
}
],
"error": {
"code": "rpc_unavailable",
"message": "acme.example wants you to sign in with your Ethereum account: …"
}
}
]
}

Enable a token (chain) this wallet accepts

POST /accounts/{account_id}/wallets/{id}/tokens

Bearer JWT (Sign-In With Ethereum)

Upserts an accepted token/chain combination for the wallet — the same combinations exposed by GET /payment_methods and enforced by POST /payments. Re-enabling a previously-disabled holding reactivates its row rather than duplicating it (200 vs 201 on a first enable). THE DEFAULT: a wallet that accepts anything keeps exactly one, so the FIRST token enabled takes the role unasked, and default: true here is how it is switched OVER — the successor is promoted and the incumbent demoted in the same request. default: false on the CURRENT default is refused 422 default_payment_method: clearing it without naming a successor would leave the wallet with no preferred method and nothing saying so. Owner only, and only on the wallet the session signed in with: another wallet of the same account is refused 403 not_your_wallet.

Path parameters

  • account_id string (uuid) required
  • id string required

    Wallet id (UUID) or 0x address.

Request body

  • chain_id integer required

    EVM chain id of the token.

  • token string required

    Token address (0x, 40 hex).

  • default boolean

    Make this the wallet's default token.

Responses

  • 200 Existing holding re-enabled/updated.
  • 201 Holding created.
  • 401 Authentication failed.
  • 403 Caller not permitted.
  • 404 Resource not found.
  • 422 Unknown chain or token.
Terminal window
curl -X POST "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/tokens" \
-H "Authorization: Bearer $RAIL0_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"chain_id": 84532,
"token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
}'
Response 200
{
"token_id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"token": {
"chain_id": 84532,
"symbol": "USDC",
"address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"decimals": 6,
"active": false
},
"active": false,
"default": false
}

Disable a token this wallet accepts (soft delete)

DELETE /accounts/{account_id}/wallets/{id}/tokens/{token_id}

Bearer JWT (Sign-In With Ethereum)

Soft-disables the accepted token/chain holding (active:false), removing it from discovery/creation while keeping the row. Re-enable via POST. Refused 422 default_payment_method when the holding is the wallet's ACTIVE default and the wallet accepts other methods — switch the default over first (POST with default:true on its successor). The LAST active method can be disabled even as the default: the wallet then accepts nothing, and the flag is cleared with it. An INACTIVE holding still carrying the flag is not protected: that is a row already stood down when its chain left service. Owner only, and only on the wallet the session signed in with: another wallet of the same account is refused 403 not_your_wallet.

Path parameters

  • account_id string (uuid) required
  • id string required

    Wallet id (UUID) or 0x address.

  • token_id string (uuid) required

    Token UUID.

Responses

  • 204 Holding disabled.
  • 401 Authentication failed.
  • 403 Caller not permitted.
  • 404 Resource not found.
  • 422 The holding is the wallet's ACTIVE default payment method and the wallet accepts other methods. code is default_payment_method — switch the default over first (POST .../tokens with default:true on the successor, which demotes this one in the same request).
Terminal window
curl -X DELETE "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/tokens/3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 204 · No content

Enable an existing token holding

PATCH /accounts/{account_id}/wallets/{id}/tokens/{token_id}/enable

Bearer JWT (Sign-In With Ethereum)

Enables an existing holding (active:true). Returns 404 if the wallet has no holding for the token (use POST to create one). Owner only, and only on the wallet the session signed in with: another wallet of the same account is refused 403 not_your_wallet.

Path parameters

  • account_id string (uuid) required
  • id string required

    Wallet id (UUID) or 0x address.

  • token_id string (uuid) required

    Token UUID.

Responses

  • 200 Holding enabled.
  • 401 Authentication failed.
  • 403 Caller not permitted.
  • 404 Resource not found.
Terminal window
curl -X PATCH "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/tokens/3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f/enable" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
{
"token_id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"token": {
"chain_id": 84532,
"symbol": "USDC",
"address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"decimals": 6,
"active": false
},
"active": false,
"default": false
}

Disable an existing token holding

PATCH /accounts/{account_id}/wallets/{id}/tokens/{token_id}/disable

Bearer JWT (Sign-In With Ethereum)

Disables an existing holding (active:false). Returns 404 if the wallet has no holding for the token, and 422 default_payment_method when it is the wallet's active default and other methods are active — switch the default over first. The last active method can be disabled even as the default (the flag is cleared with it). Owner only, and only on the wallet the session signed in with: another wallet of the same account is refused 403 not_your_wallet.

Path parameters

  • account_id string (uuid) required
  • id string required

    Wallet id (UUID) or 0x address.

  • token_id string (uuid) required

    Token UUID.

Responses

  • 200 Holding disabled.
  • 401 Authentication failed.
  • 403 Caller not permitted.
  • 404 Resource not found.
  • 422 The holding is the wallet's ACTIVE default payment method and the wallet accepts other methods. code is default_payment_method — switch the default over first (POST .../tokens with default:true on the successor, which demotes this one in the same request).
Terminal window
curl -X PATCH "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/tokens/3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f/disable" \
-H "Authorization: Bearer $RAIL0_TOKEN"
Response 200
{
"token_id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f",
"token": {
"chain_id": 84532,
"symbol": "USDC",
"address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
"decimals": 6,
"active": false
},
"active": false,
"default": false
}