API reference
Wallets
Account-scoped wallet and wallet-token management.
- GET /accounts/{account_id}/wallets
- POST /accounts/{account_id}/wallets
- GET /accounts/{account_id}/wallets/{id}
- PATCH /accounts/{account_id}/wallets/{id}
- DELETE /accounts/{account_id}/wallets/{id}
- GET /accounts/{account_id}/wallets/{id}/balances
- POST /accounts/{account_id}/wallets/{id}/tokens
- DELETE /accounts/{account_id}/wallets/{id}/tokens/{token_id}
- PATCH /accounts/{account_id}/wallets/{id}/tokens/{token_id}/enable
- PATCH /accounts/{account_id}/wallets/{id}/tokens/{token_id}/disable
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).
curl "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets" \ -H "Authorization: Bearer $RAIL0_TOKEN"const res = await fetch("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets", { headers: { Authorization: `Bearer ${process.env.RAIL0_TOKEN}`, },});
if (!res.ok) { const { code, detail } = await res.json(); throw new Error(`${res.status} ${code}: ${detail}`);}const data = await res.json();require "json"require "net/http"
uri = URI("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets")req = Net::HTTP::Get.new(uri)req["Authorization"] = "Bearer #{ENV.fetch("RAIL0_TOKEN")}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }raise "#{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)data = JSON.parse(res.body)package main
import ( "fmt" "io" "net/http" "os")
func main() { url := "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets" req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("RAIL0_TOKEN"))
res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close()
data, err := io.ReadAll(res.Body) if err != nil { panic(err) } fmt.Println(res.Status, string(data))}[ { "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
statementmust be exactly "Add this wallet to your RAIL0 account" — a login-shaped proof is refused with 422siwe_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.
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"}'const res = await fetch("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets", { method: "POST", headers: { Authorization: `Bearer ${process.env.RAIL0_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ address: "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", message: "acme.example wants you to sign in with your Ethereum account: …", signature: "0x6b1f…9c1b" }),});
if (!res.ok) { const { code, detail } = await res.json(); throw new Error(`${res.status} ${code}: ${detail}`);}const data = await res.json();require "json"require "net/http"
uri = URI("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets")req = Net::HTTP::Post.new(uri)req["Authorization"] = "Bearer #{ENV.fetch("RAIL0_TOKEN")}"req["Content-Type"] = "application/json"req.body = { address: "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", message: "acme.example wants you to sign in with your Ethereum account: …", signature: "0x6b1f…9c1b"}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }raise "#{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)data = JSON.parse(res.body)package main
import ( "fmt" "io" "net/http" "os" "strings")
func main() { body := strings.NewReader(`{ "address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", "message": "acme.example wants you to sign in with your Ethereum account: …", "signature": "0x6b1f…9c1b"}`) url := "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets" req, err := http.NewRequest(http.MethodPost, url, body) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("RAIL0_TOKEN")) req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close()
data, err := io.ReadAll(res.Body) if err != nil { panic(err) } fmt.Println(res.Status, string(data))}{ "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.
curl "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b" \ -H "Authorization: Bearer $RAIL0_TOKEN"const res = await fetch("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b", { headers: { Authorization: `Bearer ${process.env.RAIL0_TOKEN}`, },});
if (!res.ok) { const { code, detail } = await res.json(); throw new Error(`${res.status} ${code}: ${detail}`);}const data = await res.json();require "json"require "net/http"
uri = URI("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b")req = Net::HTTP::Get.new(uri)req["Authorization"] = "Bearer #{ENV.fetch("RAIL0_TOKEN")}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }raise "#{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)data = JSON.parse(res.body)package main
import ( "fmt" "io" "net/http" "os")
func main() { url := "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b" req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("RAIL0_TOKEN"))
res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close()
data, err := io.ReadAll(res.Body) if err != nil { panic(err) } fmt.Println(res.Status, string(data))}{ "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.
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 '{}'const res = await fetch("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b", { method: "PATCH", headers: { Authorization: `Bearer ${process.env.RAIL0_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({}),});
if (!res.ok) { const { code, detail } = await res.json(); throw new Error(`${res.status} ${code}: ${detail}`);}const data = await res.json();require "json"require "net/http"
uri = URI("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b")req = Net::HTTP::Patch.new(uri)req["Authorization"] = "Bearer #{ENV.fetch("RAIL0_TOKEN")}"req["Content-Type"] = "application/json"req.body = {}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }raise "#{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)data = JSON.parse(res.body)package main
import ( "fmt" "io" "net/http" "os" "strings")
func main() { body := strings.NewReader(`{}`) url := "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b" req, err := http.NewRequest(http.MethodPatch, url, body) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("RAIL0_TOKEN")) req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close()
data, err := io.ReadAll(res.Body) if err != nil { panic(err) } fmt.Println(res.Status, string(data))}{ "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.
curl -X DELETE "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b" \ -H "Authorization: Bearer $RAIL0_TOKEN"const res = await fetch("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b", { method: "DELETE", headers: { Authorization: `Bearer ${process.env.RAIL0_TOKEN}`, },});
if (!res.ok) { const { code, detail } = await res.json(); throw new Error(`${res.status} ${code}: ${detail}`);}require "json"require "net/http"
uri = URI("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b")req = Net::HTTP::Delete.new(uri)req["Authorization"] = "Bearer #{ENV.fetch("RAIL0_TOKEN")}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }raise "#{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)package main
import ( "fmt" "io" "net/http" "os")
func main() { url := "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b" req, err := http.NewRequest(http.MethodDelete, url, nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("RAIL0_TOKEN"))
res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close()
data, err := io.ReadAll(res.Body) if err != nil { panic(err) } fmt.Println(res.Status, string(data))}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.
curl "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/balances" \ -H "Authorization: Bearer $RAIL0_TOKEN"const res = await fetch("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/balances", { headers: { Authorization: `Bearer ${process.env.RAIL0_TOKEN}`, },});
if (!res.ok) { const { code, detail } = await res.json(); throw new Error(`${res.status} ${code}: ${detail}`);}const data = await res.json();require "json"require "net/http"
uri = URI("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/balances")req = Net::HTTP::Get.new(uri)req["Authorization"] = "Bearer #{ENV.fetch("RAIL0_TOKEN")}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }raise "#{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)data = JSON.parse(res.body)package main
import ( "fmt" "io" "net/http" "os")
func main() { url := "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/balances" req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("RAIL0_TOKEN"))
res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close()
data, err := io.ReadAll(res.Body) if err != nil { panic(err) } fmt.Println(res.Status, string(data))}{ "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.
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"}'const res = await fetch("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/tokens", { method: "POST", headers: { Authorization: `Bearer ${process.env.RAIL0_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ chain_id: 84532, token: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" }),});
if (!res.ok) { const { code, detail } = await res.json(); throw new Error(`${res.status} ${code}: ${detail}`);}const data = await res.json();require "json"require "net/http"
uri = URI("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/tokens")req = Net::HTTP::Post.new(uri)req["Authorization"] = "Bearer #{ENV.fetch("RAIL0_TOKEN")}"req["Content-Type"] = "application/json"req.body = { chain_id: 84532, token: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"}.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }raise "#{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)data = JSON.parse(res.body)package main
import ( "fmt" "io" "net/http" "os" "strings")
func main() { body := strings.NewReader(`{ "chain_id": 84532, "token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e"}`) url := "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/tokens" req, err := http.NewRequest(http.MethodPost, url, body) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("RAIL0_TOKEN")) req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close()
data, err := io.ReadAll(res.Body) if err != nil { panic(err) } fmt.Println(res.Status, string(data))}{ "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.
codeisdefault_payment_method— switch the default over first (POST .../tokens with default:true on the successor, which demotes this one in the same request).
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"const res = await fetch("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/tokens/3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f", { method: "DELETE", headers: { Authorization: `Bearer ${process.env.RAIL0_TOKEN}`, },});
if (!res.ok) { const { code, detail } = await res.json(); throw new Error(`${res.status} ${code}: ${detail}`);}require "json"require "net/http"
uri = URI("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/tokens/3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f")req = Net::HTTP::Delete.new(uri)req["Authorization"] = "Bearer #{ENV.fetch("RAIL0_TOKEN")}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }raise "#{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)package main
import ( "fmt" "io" "net/http" "os")
func main() { url := "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/tokens/3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f" req, err := http.NewRequest(http.MethodDelete, url, nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("RAIL0_TOKEN"))
res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close()
data, err := io.ReadAll(res.Body) if err != nil { panic(err) } fmt.Println(res.Status, string(data))}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.
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"const res = await fetch("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/tokens/3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f/enable", { method: "PATCH", headers: { Authorization: `Bearer ${process.env.RAIL0_TOKEN}`, },});
if (!res.ok) { const { code, detail } = await res.json(); throw new Error(`${res.status} ${code}: ${detail}`);}const data = await res.json();require "json"require "net/http"
uri = URI("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/tokens/3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f/enable")req = Net::HTTP::Patch.new(uri)req["Authorization"] = "Bearer #{ENV.fetch("RAIL0_TOKEN")}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }raise "#{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)data = JSON.parse(res.body)package main
import ( "fmt" "io" "net/http" "os")
func main() { url := "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/tokens/3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f/enable" req, err := http.NewRequest(http.MethodPatch, url, nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("RAIL0_TOKEN"))
res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close()
data, err := io.ReadAll(res.Body) if err != nil { panic(err) } fmt.Println(res.Status, string(data))}{ "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.
codeisdefault_payment_method— switch the default over first (POST .../tokens with default:true on the successor, which demotes this one in the same request).
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"const res = await fetch("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/tokens/3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f/disable", { method: "PATCH", headers: { Authorization: `Bearer ${process.env.RAIL0_TOKEN}`, },});
if (!res.ok) { const { code, detail } = await res.json(); throw new Error(`${res.status} ${code}: ${detail}`);}const data = await res.json();require "json"require "net/http"
uri = URI("https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/tokens/3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f/disable")req = Net::HTTP::Patch.new(uri)req["Authorization"] = "Bearer #{ENV.fetch("RAIL0_TOKEN")}"
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(req) }raise "#{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)data = JSON.parse(res.body)package main
import ( "fmt" "io" "net/http" "os")
func main() { url := "https://api.rail0.xyz/accounts/a1b2c3d4-0000-4e5f-8a9b-0c1d2e3f4a5b/wallets/5f0c2b1e-8d4a-4f6e-9a7b-2c3d4e5f6a7b/tokens/3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f/disable" req, err := http.NewRequest(http.MethodPatch, url, nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("RAIL0_TOKEN"))
res, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer res.Body.Close()
data, err := io.ReadAll(res.Body) if err != nil { panic(err) } fmt.Println(res.Status, string(data))}{ "token_id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f", "token": { "chain_id": 84532, "symbol": "USDC", "address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", "decimals": 6, "active": false }, "active": false, "default": false}