Keel node HTTP/WS API (v1)
Base: http://127.0.0.1:<rpc-port> (devnet:
5000+i). JSON everywhere. Amounts are integers in smallest units and may
exceed 2^53: parse as bigint. Addresses are 64-hex ed25519 public keys
in paths; inside a signed action's JSON they are 32-element byte arrays
(serde form).
| Method | Path | Returns |
|---|---|---|
| POST | /v1/actions |
body: SignedAction JSON
({envelope:{signer,nonce,chain_id,action},signature}) → 202
{tx_id, admitted:true} or 422
{admitted:false, code, error} |
| GET | /v1/status |
{chain_id, height, timestamp, state_hash, validators[], mempool, accounts, orders} |
| GET | /v1/params |
governance parameters |
| GET | /v1/accounts/{addr} |
{nonce, budget{used,limit,cancel_limit,remaining,earned}, tier, tier_expires_at, balances[{asset,account_type,balance}]} |
| GET | /v1/accounts/{addr}/orders |
{open:[OrderRecord]} |
| GET | /v1/accounts/{addr}/trades |
{trades:[Trade]} |
| GET | /v1/markets |
{markets:[summary]} |
| GET | /v1/markets/{pair} |
summary: cfg, house quote, last price, best bid/ask, volumes |
| GET | /v1/markets/{pair}/book?depth=20 |
{bids:[{price,size,orders}], asks:[…], house, height} |
| GET | /v1/orders/{id} |
OrderRecord |
| GET | /v1/offers?asset=&side=&fiat= |
{offers:[Offer]} (open offers only, each with
arbitrators[]) |
| GET | /v1/offers/{id} |
Offer, plus arbitrators[]: the bonded arbitrators the
owner named (SetOfferArbitrators; empty = the elected set
rules) |
| GET | /v1/trades/{id} |
Trade (+ dispute when one exists),
arbitrator (bound to rule it, or null) and
arbitrator_candidates[] (copied from the offer at
start) |
| GET | /v1/vaults/{chain} |
{chain, vault, state} (vault registrations for the
chain; whole vaults sub-state) |
| GET | /v1/vaults/{chain}/addresses?from=0&limit=100 |
deposit index → owner rows |
| GET | /v1/vaults/outbounds?status= |
{outbounds:[Outbound]} |
| GET | /v1/gov/proposals |
{proposals:[Proposal], house_operator} |
| GET | /v1/gov/proposals/{id} |
Proposal |
| GET | /v1/staking/validators |
{consensus:[hex keys], staked:[{consensus_key,power}], staking} |
| GET | /v1/receipts/{tx_id} |
{index, tx_id, signer, ok, error{code,message}?, events[]}
(404 until applied) |
| GET | /v1/clients/{addr} →
{address, attester, fee, caps, earned, usage_paid, attested_accounts, usage_prices} |
a client's retail schedule and what it earned and paid |
| GET | /v1/treasury →
{height, balances:{treasury,validator_rewards,burn}, last_buyback, params, fee_split_bps} |
system balances per asset and the epoch buyback record |
| GET | /v1/ready/{chain}?max_checkpoint_age= →
{chain, ready, reasons, height, vault, checkpoint, last_deposit_credited, outbound_pending, halted, fee_rate} |
whether a client may turn that network on; what a per-network switch polls |
| GET | /v1/sync/meta →
{height, state_hash, last_hash, schema, boundaries:[{epoch,height,digest}]} |
newest snapshot for state sync
(keel-node --sync-from) |
| GET | /v1/sync/snapshot → bytes |
the snapshot itself (KEEL header, schema, borsh
state) |
| GET | /v1/blocks/{height}/receipts |
{height, receipts[]} (last 10,000 blocks) |
| WS | /v1/ws |
one JSON message per applied block:
{height, timestamp, state_hash, receipts[], events[], books{pair:{best_bid,best_ask,last_price}}} |
Sub-states owned by other modules (offers, trades, vaults, proposals,
staking) are served as their serde JSON; field names follow the Rust
structs in crates/keel-vm/src/modules/.
Error codes on /v1/actions: BAD_SIGNATURE,
WRONG_CHAIN, BAD_NONCE,
BUDGET_EXHAUSTED, BLOCK_CAP,
PAUSED, UNAUTHORIZED, INVALID. A
nonce may run up to 64 ahead of the chain nonce (pipelining); actions
are selected per signer in nonce order.
Custody (client-owned vaults)
| Route | What |
|---|---|
GET /v1/custody |
one entry per live generation of every client-owned
vault (active first): custodian, chain,
gen, status
(Active/Draining), sweep,
drain_until, swept[],
pending_sweeps[] (sweeps reported by one observer that the
others verify on their own nodes and vote on: from_gen,
tx_hash, asset, amount,
fee, votes), vault (that
generation's key, chain code, epoch), signer_url,
address (its index 0), next_deposit_index,
deposit_owners, reserves[]
(asset, reserve, liabilities,
halted) |
GET /v1/custody/{addr} |
one client's live generations and reserves, rotations[]
pending (RotateCustodyVault with activate_at),
the number of accounts in its custody, and floats[]
(asset, custody, deposit: the
custodian's balance on each side, what SwapCustody can draw
on) |
GET /v1/custody/{addr}/{chain}/addresses?from&limit&owner&gen |
deposit addresses of that vault with their owners, from the active
generation's key (gen picks a draining one); rows carry
gen |
GET /v1/custody/{addr}/{chain}/addresses/lookup?address= |
which index, owner and generation a custody address belongs to, across every live generation |
Outbound rows and batches under /v1/vaults/outbounds
carry custodian (null for the network vault).
WebSocket
/v1/ws carries filtered, sequenced subscriptions
(blocks, account:, pair:,
deposits:, book:) with a gap
frame when a client falls behind and replay from a height; the protocol
is in API-WS.md. A socket that never subscribes receives
blocks, which is the earlier per-block feed.
Stability
/v1 is frozen: routes and fields are added, never
removed or renamed. A deprecation is announced thirty days ahead in the
changelog and keeps working meanwhile. Action submission may require an
API key (Authorization: Bearer) on the public testnet;
reads never do.