Skip to content

HTTP API conventions

Base URL, authentication and access levels, error bodies, the empty-204 rule and the body cap: the rules every Queen route follows.

Updated View as Markdown

Queen’s broker speaks plain HTTP/1.1 with JSON bodies. There is no binary protocol, no persistent session, and no client-side coordination state. Anything that can issue an HTTP request is a first-class client. The rules on this page hold for every route; the per-route pages only document what is specific to that route.

Base URL and versioning

The broker binds 0.0.0.0 on PORT, which defaults to 6632. A local broker is therefore at http://localhost:6632.

Prefix What lives there
/api/v1/… the whole public API: message plane, queues, consumer groups, messages, DLQ, traces, status, analytics, system
/streams/v1/… the stream-processing surface
/internal/api/… broker-to-broker calls; not a client API
/health, /metrics, /metrics/prometheus, /status operational endpoints
everything else the embedded dashboard SPA, served from assets compiled into the binary

There is exactly one API version, v1, and version 1.0.0 of the broker and of every client SDK are released together. A path is the version: there is no Accept header negotiation and no version query parameter.

Anything the router does not match falls through to the dashboard. That fallback answers a JSON 404 ({"error":"Not Found","code":"no_such_route"}) for any path starting with /api/ and for any non-GET/HEAD request, so a call to a route that does not exist fails loudly instead of returning the SPA’s HTML with a 200.

Authentication

JWT bearer authentication is off by default (JWT_ENABLED=false); with it off, every route is reachable unauthenticated and the broker relies entirely on network isolation. With it on, send the token in the standard header:

Authorization: Bearer <jwt>

A bare token with no Bearer prefix is also accepted. Supported algorithms are HS256/384/512, RS256/384/512 and EdDSA, selected by JWT_ALGORITHM (or auto); the verification key comes from a static PEM or from a JWKS endpoint cached per kid. Role claim names are remappable. The configuration page in Reference lists every JWT_* variable, and Self-hosting covers key distribution.

Two failure codes, and they mean different things:

Code Meaning
401 no token, unparseable token, bad signature, expired, not yet valid, or a rejected issuer/audience. Body {"error":"Authentication required"} or a specific token message
403 the token is valid but its roles do not satisfy the route’s access level. Body {"error":"Insufficient permissions"}

Access levels are a role set, not a ladder

Every route is mapped to one of five access levels. The mapping from a token’s roles to the levels it satisfies is not hierarchical: write-only is not a weaker read-write, it is a different capability. This matters most for producers.

Route’s access level Satisfied by role Not satisfied by
public anyone, with or without a token nothing
read-only read-only, read-write, admin write-only
write-only write-only, read-write, admin read-only
read-write read-write, admin read-only, write-only
admin admin everything else

POST /api/v1/push is the only write-only route. So a token whose single role is write-only can produce and can do nothing else: every read route and every consume route (/pop, /ack, /transaction, /lease/:leaseId/extend) is read-write and returns 403 for it. Conversely a read-only token cannot pop, because popping takes a lease and mutates a cursor.

Role names come from the role claim and the roles array claim, and both the claim names and the four role names are configurable. A path the access table does not recognise defaults to read-write.

The public routes, reachable with no token even when authentication is on, are /health, /metrics, /metrics/prometheus, /, /assets/*, /favicon*, and the three dashboard-identity routes /auth/me, /auth/login and /auth/logout. The last three are public so they can answer the auth question themselves: with JWT on, /auth/me returns 401 from its own handler and /auth/login serves the page that says the dashboard needs the proxy. /metrics/prometheus being public is deliberate for scraping and is a hazard in a multi-tenant deployment, where per-queue series sum across tenants; see Self-hosting.

A second, independent exemption list is JWT_SKIP_PATHS, which defaults to /health,/metrics/prometheus,/metrics,/. Entries match a path exactly, or as a prefix when the entry ends in /. A path on that list skips authentication entirely, so widening it widens the unauthenticated surface.

Requests

Bodies are JSON. The broker reads the raw bytes and parses them as JSON without inspecting Content-Type, so sending application/json is good manners rather than a requirement. A body that does not parse returns 400 with the parser’s message:

{"error":"bad body: expected value at line 1 column 1"}

Query parameters on the message-plane routes are typed, not free-form strings: batch=abc is a deserialization failure for the whole query string rather than a silently-ignored parameter, and it is rejected by the extractor before the handler runs. Management routes that take filter parameters read them as strings and additionally accept 1/0 and yes/no for booleans.

The request body cap is 64 MiB (QUEEN_MAX_BODY_BYTES, default 67108864). A larger body is rejected by the body-limit layer before any handler runs. The cap applies per request, so a push of many small messages is bounded by the sum of their payloads plus JSON framing.

Responses

Successful API responses carry Content-Type: application/json. The exception is /metrics/prometheus, which returns text/plain; version=0.0.4; charset=utf-8 with Cache-Control: no-cache.

Every 204 has no body at all

When a pop has nothing to deliver, the broker returns 204 No Content with no body and no Content-Length. This is deliberate, and it is not cosmetic: announcing a content length for a body that hyper then elides makes strict HTTP/1.1 clients (Node’s undici/llhttp in particular) treat the connection as poisoned and close it, which under empty-poll load snowballs into ECONNRESET storms. The bodies that were dropped carried nothing a client read.

Consequences you have to design around:

  • Check the status code before parsing. A 204 has no JSON to parse.
  • Any diagnostic the handler wanted to attach to an empty result is lost. Pop maintenance mode is the clearest case: the handler builds {"messages":[],"paused":true}, and the client sees a bare 204 indistinguishable from an empty queue.
  • An error reported inside a pop stored procedure’s result also renders as an empty pop, and therefore as a 204 with no text. Genuine transport-level failures (no pool connection, a statement timeout) come back as 500 with a body.

Errors

Every error the broker produces itself is a flat object with one key:

{"error":"invalid or expired lease"}

The message is JSON-escaped, so it is always parseable. There is no error code enum, no details object and no retryAfter; the HTTP status plus this string is the whole contract.

Some routes return a stored procedure’s JSON verbatim

Queen’s management surface is implemented in PostgreSQL. Many routes (/api/v1/configure and most of the resources, status, analytics, consumer-groups, messages, dlq and traces handlers) pass the stored procedure’s JSON straight through without reshaping it, because clients assert on those exact keys. Two things follow:

  • The shape of those responses is the stored procedure’s shape, including keys that only make sense internally.
  • When such a result carries an error key, the broker maps it to a status code by inspecting the text: 404 if the message contains not found (case-insensitive), otherwise 500. The original body is preserved.

The message-plane routes on the following pages are the opposite: their bodies are assembled by hand in the broker, field by field, and the fields that are absent are as load-bearing as the ones that are present.

A 2xx does not mean every item succeeded

/api/v1/push, /api/v1/ack and /api/v1/ack/batch answer with a top-level JSON array, one element per request item, in request order. The HTTP status describes the call; each element describes one item.

Route Status on a processed call Where the real outcome is
POST /api/v1/push 201 each element’s status: queued, duplicate, buffered or failed
POST /api/v1/ack, /ack/batch 200 each element’s success, error, noop, dlq, leaseReleased
POST /api/v1/transaction 200 top-level success; a failure is success:false with error and an empty results

A client that only checks res.ok will silently lose messages: a push whose database transaction failed still returns 201, with the affected items marked buffered (spooled to disk for replay) or failed (not stored anywhere).

Status codes used by the message plane

Code When
200 a pop that delivered messages; every ack; a lease extension; a transaction that was processed, including one that rolled back
201 every push the broker processed, including one whose items were spooled to disk
204 a pop with nothing to deliver, pop maintenance, or a pop whose stored procedure reported an error. No body
400 unparseable body, a missing required field, an unknown transaction operation type, a discovery pop with neither namespace nor task, or a malformed x-queen-tenant when tenancy is on
401 / 403 authentication and authorization, as above
404 a path the router does not match, or a stored-procedure error whose text contains not found
413 the request body exceeds the cap
500 no PostgreSQL connection available ({"error":"pool"}), a pop that failed at the transport level, a failed lease renewal, or a stored-procedure error that is not a not-found
503 /health only, when the database round-trip fails

The broker never returns 429: it has no per-client rate limiter. Its internal push and pop concurrency limits are adaptive (Vegas) and make a request wait for a permit rather than rejecting it. Rate limiting and 429 responses come from the multi-tenant gateway, not from the broker.

Retrying safely

Operation Safe to retry blindly Why
POST /api/v1/push yes, if you send your own transactionId deduplication is exact and window-bounded, so the retry returns status:"duplicate" and writes nothing
GET /api/v1/pop no a pop takes a lease and may deliver a different batch; retrying is a new claim, not a repeat
POST /api/v1/ack yes an ack that lands below the cursor is reported noop:true rather than double-committing
POST /api/v1/lease/:leaseId/extend yes renewal never shortens a lease
POST /api/v1/transaction only with deterministic transactionIds the whole bundle re-runs; dedup is what makes the pushes idempotent

The tenant-scoped column

The generated route table has a Tenant-scoped column. It records whether that handler resolves a tenant identity for the request and threads it into the SQL that resolves queues by name. Native tenant scoping is off by default; with it off, every request resolves to a single default tenant and the column tells you which handlers would participate if it were on. It is an operator-level feature of multi-tenant deployments, not a client-facing one. Self-hosting and Internals cover it, including which routes are not scoped and what that implies.

Every route

Queen’s broker registers 63 method + path pairs. Every row below is read out of the router and the authorization table at build time.

Message plane

Method Path Access level Tenant-scoped
POST /api/v1/ack read-write yes
POST /api/v1/ack/batch read-write yes
POST /api/v1/lease/:leaseId/extend read-write yes
GET /api/v1/pop read-write yes
GET /api/v1/pop/queue/:queue read-write yes
GET /api/v1/pop/queue/:queue/partition/:partition read-write yes
POST /api/v1/push write-only yes
POST /api/v1/transaction read-write yes

Queues and partitions

Method Path Access level Tenant-scoped
POST /api/v1/configure read-write yes
GET /api/v1/resources/namespaces read-only yes
GET /api/v1/resources/overview read-only yes
GET /api/v1/resources/queues read-only yes
DELETE /api/v1/resources/queues/:queue admin yes
GET /api/v1/resources/queues/:queue read-only yes
GET /api/v1/resources/tasks read-only yes

Consumer groups

Method Path Access level Tenant-scoped
GET /api/v1/consumer-groups read-only yes
DELETE /api/v1/consumer-groups/:group admin yes
GET /api/v1/consumer-groups/:group read-only yes
DELETE /api/v1/consumer-groups/:group/queues/:queue admin yes
POST /api/v1/consumer-groups/:group/queues/:queue/partitions/:partition/seek read-write yes
POST /api/v1/consumer-groups/:group/queues/:queue/seek read-write yes
POST /api/v1/consumer-groups/:group/subscription read-write yes
GET /api/v1/consumer-groups/lagging read-only yes

Messages, DLQ and traces

Method Path Access level Tenant-scoped
GET /api/v1/dlq read-only yes
GET /api/v1/messages read-only yes
DELETE /api/v1/messages/:partitionId/:transactionId read-write yes
GET /api/v1/messages/:partitionId/:transactionId read-only yes
POST /api/v1/messages/:partitionId/:transactionId/retry read-write yes
POST /api/v1/traces read-write yes
GET /api/v1/traces/:partitionId/:transactionId read-only yes
GET /api/v1/traces/by-name/:traceName read-only yes
GET /api/v1/traces/names read-only yes

Status, metrics and analytics

Method Path Access level Tenant-scoped
GET /api/v1/analytics/postgres-stats read-only no
GET /api/v1/analytics/queue-lag read-only yes
GET /api/v1/analytics/queue-ops read-only yes
GET /api/v1/analytics/queue-parked-replicas read-only yes
GET /api/v1/analytics/retention read-only yes
GET /api/v1/analytics/system-metrics read-only no
GET /api/v1/analytics/worker-metrics read-only no
POST /api/v1/stats/refresh admin no
GET /api/v1/status read-only no
GET /api/v1/status/analytics read-only yes
GET /api/v1/status/buffers read-only no
GET /api/v1/status/queues read-only yes
GET /api/v1/status/queues/:queue read-only yes
GET /health public no
GET /metrics public no
GET /metrics/prometheus public no
GET /status read-only no

Streams

Method Path Access level Tenant-scoped
POST /streams/v1/cycle read-write no
POST /streams/v1/queries read-write no
POST /streams/v1/state/get read-only no

Operator surfaces

Method Path Access level Tenant-scoped
GET /api/v1/system/maintenance admin no
POST /api/v1/system/maintenance admin no
GET /api/v1/system/maintenance/pop admin no
POST /api/v1/system/maintenance/pop admin no
GET /api/v1/system/shared-state admin no

Dashboard identity (broker-direct)

Method Path Access level Tenant-scoped
GET /auth/login public no
POST /auth/logout public no
GET /auth/me public no

Internal (broker-to-broker)

Method Path Access level Tenant-scoped
GET /internal/api/inter-instance/stats admin no
POST /internal/api/notify admin yes
GET /internal/api/shared-state/stats admin no

40 of these handlers resolve a tenant from the request; 11 /api/v1/* routes do not and are therefore cell-wide reads or operator surfaces.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close