API reference
The signed request contract, every scope and which endpoint needs it, the public and bot-facing REST surface, the WebSocket streams and their handshakes, the admin endpoints with their permissions, and the wire dialect that catches integrators out.
Three surfaces, three authentication models. /api/hb/* bot paths are
HMAC-signed. A carve-out of /api/hb/* browser paths use the ordinary
session. Everything under /api/admin/hb/* is a session plus an explicit
permission.
The convention that will catch you out
The platform normally pins HTTP at 200 and puts the outcome in the body. The
Hummingbot bot-facing endpoints deliberately do the opposite: they speak the
Binance dialect, with real status codes and a {code, msg} error envelope, and
they skip the CORS middleware that would otherwise lock the status line at 200.
That is what lets a genuine 429 or 423 reach a bot's error parser.
The browser-facing carve-out — /api/hb/keys, /api/hb/strategy,
/api/hb/setup, /api/hb/console and /api/hb/connector, with everything under
them — keeps the platform dialect ({message, statusCode}), because $fetch
cannot read a Binance envelope. Which dialect you get is decided by the path, not
by the caller.
Signing a request
Four headers on every signed call:
| Header | Value |
|---|---|
X-API-Key |
The public key string |
X-Timestamp |
Unix milliseconds |
X-Nonce |
16–128 hex characters, single-use |
X-Signature |
Hex HMAC-SHA256 of the payload below |
METHOD\npath\ntimestamp\nnonce\nrawBodypath is the URL pathname only — no query string. Query parameters are
already part of the request line, and keeping them out of the payload keeps
composition stable across proxies that rewrite trailing slashes. rawBody is the
empty string when there is no body.
payload = f"{method.upper()}\n{path}\n{ts}\n{nonce}\n{body or ''}"
sig = hmac.new(secret.encode(), payload.encode(), hashlib.sha256).hexdigest()Rules the server enforces, in this order:
- Timestamp sanity. Anything more than five minutes out is refused before the key is looked up at all, so probe traffic never reaches account records.
- Key lookup, then signature verification. Everything below this point is reachable only by a caller who holds the secret — which is why an attacker who merely knows a public key string cannot probe its state or drive audit writes.
- Expiry → 401. Disabled → 423 Locked, carrying the operator's or the customer's own reason as the message.
- Receive window → 401, naming the direction and size of the drift. Default ±10 seconds.
- IP allowlist, when the key has one. Deny by default: a request whose source IP cannot be determined, or a key whose allowlist is empty, is refused.
- Replay. Nonces are held in Redis for twice the receive window; a repeat is
401
Replayed nonce.
Scopes
| Scope | Authorises |
|---|---|
hb:read:account |
Balances, order lookups, order lists |
hb:read:market |
Signed market-data reads |
hb:trade:spot |
Placing spot orders |
hb:cancel:spot |
Cancelling and amending spot orders |
hb:trade:perp |
Placing perpetual orders |
hb:cancel:perp |
Cancelling and amending perpetual orders |
hb:perp:positions |
Reading positions and setting leverage |
hb:control:bot |
Driving the customer's own bot through the local agent |
Presets offered on the key form: read-only, spot-trade, perp-trade,
bot-control, full-access.
Every other scope authorises an action on your exchange. This one authorises an action on their machine, so it is the only scope whose blast radius is outside the platform — which is why it is absent from both trading presets. Existing keys never gain it: permissions are stored as a literal array at creation.
Read-only mode blocks any scope that does not begin hb:read:.
Public endpoints
No authentication. Metered per client IP against the market-data budget.
Symbols are dash-separated on the wire (BTC-USDT); the platform stores them as
separate base and quote columns and renders BTC/USDT internally. Asking for more
depth than a market has returns the available levels rather than an error, so a
bot's initial book is never left empty by a depth request.
Signed spot endpoints
An earlier build returned everything except the live ones, so a bot reasonably concluded its book was empty and re-quoted on top of its own resting orders. The list now returns open and closed, newest first. A request that asks explicitly for open or closed is unaffected.
Signed perpetual endpoints
Leverage is validated against what the market offers, so a rejection reads
leverage 7x is not offered on BTC-USDT. Available: 1, 5, 10 rather than an
opaque refusal on every subsequent order.
This addon owns no book. With Ecosystem absent every spot path returns a clean 503; with Futures absent every perpetual path does. The addon's own endpoints — ping, time, keys, strategy presets — keep working either way.
Rate-limit buckets
| Bucket | Governs |
|---|---|
trade |
Order placement, cancellation, amendment, batch cancel |
account |
Balances, order lookups, order lists, positions, leverage |
read |
Public market data |
Every response carries X-RateLimit-Bucket, X-RateLimit-Limit,
X-RateLimit-Remaining and X-RateLimit-Reset; a refusal adds Retry-After.
Defaults and overrides are covered in Settings.
Browser endpoints
Session-authenticated, platform error dialect, scoped to the caller's own
account. No permission — access is gated by the view_hb KYC feature where noted.
Creating and rotating a key are additionally subject to hbRequireKyc. The
connector kit, the console, the setup state and both strategy endpoints are
gated by the view_hb KYC feature.
POST /api/hb/console/panic takes { cancelOrders?, disableKeys?, reason? },
both booleans defaulting to true. Passing both as false is a 422 — there would be
nothing to do.
WebSocket
/api/hb/stream — the connector's feed
One socket, multiplexed. Public streams need no authentication; private ones require an in-band signed handshake.
{ "action": "SUBSCRIBE", "params": ["orderbook@BTC-USDT", "trades@BTC-USDT"] }{ "action": "AUTH", "apiKey": "…", "timestamp": 1750000000000, "nonce": "…", "signature": "…" }The signature covers WS\n/api/hb/stream\n<timestamp>\n<nonce>\n. On success the
verified user id is bound to the connection and only that user's data is
delivered. A ?userId= query parameter is never trusted for identity.
| Stream | Carries |
|---|---|
orderbook@SYM, depth@SYM |
Spot book snapshots and diffs, from the Ecosystem book only |
trades@SYM, ticker@SYM |
Public spot trades and ticker |
perpOrderbook@SYM, markPrice@SYM, fundingRate@SYM |
Perpetual, from the Futures book only |
userOrders, userBalances, userPositions, userTrades |
Private, after AUTH |
Spot and perpetual books are separate channels on purpose. A shared channel that fell back from the ecosystem book to the perpetual one meant a perp connector tracking a symbol that also trades spot silently quoted off spot liquidity.
Server frames are { "stream": "...", "data": { ... } }. Both common keepalive
conventions are answered.
/api/hb/console — the browser terminal
Session-authenticated. Same path as the REST snapshot, registered separately by method. Pushes the same shape the one-shot endpoint returns.
/api/hb/agent — the local agent
Signed handshake over WS\n/api/hb/agent\n<timestamp>\n<nonce>\n, and the key
must additionally carry hb:control:bot.
| Direction | Frames |
|---|---|
| Agent → server | AUTH, HELLO, HEARTBEAT (~25s), TELEMETRY, RESULT |
| Server → agent | authAck, command, error |
Agent presence is a Redis key with a 90-second TTL refreshed by the heartbeat, and commands travel over Redis pub/sub with a local fast path — so a browser and an agent held by different backend threads still reach each other. An agent handshake deliberately does not stamp the key's "last used" field: that field is the only signal for whether a bot is reaching the exchange, and an agent is not a bot.
Admin endpoints
There is also an admin WebSocket at /api/admin/hb/instance carrying live
instance state and log lines, gated on view.hb.instance.
Settings are read and written through the platform's own endpoints
(/api/admin/system/settings), not through an addon route, which is why saving
the Hummingbot settings screen needs edit.settings.
Tables
| Table | Holds |
|---|---|
hb_strategy_preset |
Presets: family, pair, connectors, config JSON, status, version |
hb_instance |
Server-run bots: paths, preset and key links, desired and actual status, restart counters, last exit code and error |
api_key |
The keys themselves. A Hummingbot key is one carrying an hb:* scope |
api_key_audit_log |
Creation, rotation, permission changes, kill-switch events and authentication rejections |
These are the names to use in SQL. The backend refers to the last two by their
model names, apiKey and apiKeyAuditLog, which is what appears in logs and
stack traces — but neither is a table a query can name.
config, permissions, ipWhitelist and rateLimitOverride are JSON columns.
MySQL hands them back parsed; MariaDB hands them back as raw strings. The backend
normalises all of them, but a direct database consumer must handle either — and
must not treat the raw string as a list. An IP allowlist compared as text degrades
into a substring match, which is how a partial address once satisfied it.