Skip to content

This page lists every HTTP route that QueueBox serves: the inbox sources, the health routes, the metrics route and the admin routes.

Route Port without server.managementPort Port with server.managementPort
GET / server.httpPort server.httpPort
POST <inbox.basePath><path> server.httpPort server.httpPort
GET /health/live, GET /health/ready, GET /health server.httpPort server.managementPort only
GET /metrics server.httpPort server.managementPort only
POST /admin/transform/test, POST /admin/replay server.httpPort server.managementPort only

With a management port, the data port answers 404 on the health, metrics and admin paths. A path that no route serves answers 404.

Two rules apply to every route of the data port:

  • A request whose Content-Length is above inbox.maxBodyBytes gets 413 with {"error":"Request body exceeds <n> bytes"}.
  • A request that arrives during shutdown gets 503 with {"error":"QueueBox is shutting down"}.

GET / answers 200 with the text QueueBox is running!. It checks nothing. Use the health routes for a probe.

Each HTTP source of sources registers one route: POST on inbox.basePath followed by the path of the source. With the default basePath, a source with path: /stripe answers at /inbox/stripe. The source name is not part of the path.

Terminal window
curl -X POST http://localhost:8080/inbox/stripe \
-H "Content-Type: application/json" \
-d '{"id": "evt_123", "type": "payment.completed"}'
Part Rule
Body One JSON document, at most inbox.maxBodyBytes bytes. The route counts the bytes of a chunked body as it reads them.
Content-Type The route parses the body as JSON whatever the header says.
Authentication The auth block of the source, when it has one. See source auth.
X-Correlation-Id Optional. QueueBox stores it on the row. Without it, QueueBox generates a UUID.
Other headers QueueBox stores them in the headers column, except the credential headers. The header filter of the source reads them.
  1. QueueBox reads the body under the size cap.
  2. It checks the credentials.
  3. It runs the header filter.
  4. It sets the X-Correlation-Id response header.
  5. It parses the body as JSON.
  6. It extracts the idempotency key, the event type and the aggregate identifier.
  7. It runs the source transform.
  8. It stores the row. The unique constraint on (source, idempotency_key) detects a duplicate here.

The transform runs before the duplicate check. A repeat of a stored message whose transform now fails therefore gets 422, not 200.

Status Body Meaning
202 {"messageId":"<uuid>"} The message is new. QueueBox stored it. The value is the id of the inbox row.
202 {"status":"filtered"} The header filter dropped the message. QueueBox stored no row.
200 {"status":"duplicate"} The source already holds a message with this idempotency key.
400 {"error":"Invalid JSON"} The body is not JSON.
400 {"error":"<reason>"} The idempotency key path found no value.
401 {"error":"<reason>"} The request failed the authentication of the source. The reason names the check, never a credential.
413 {"error":"Request body exceeds <n> bytes"} The body is larger than inbox.maxBodyBytes.
422 {"error":"Transform failed: <reason>"} The source transform rejected the payload. QueueBox stored no row.
429 none The request went over rateLimit.requestsPerMinute. The response carries Retry-After.
500 {"error":"Storage failed"} The database write failed. Send the message again.
503 {"error":"QueueBox is shutting down"} The process is stopping. Send the message again.

A 202 means that the message is durable in the inbox, not that a destination received it. A sender can send the message again after a 500 or a 503. A 4xx answer does not change when the sender repeats the same request.

The health routes need no authentication. Each answers JSON.

Liveness. It reports the process and touches no dependency, so a slow database cannot fail it. Use it for a liveness probe.

{"status":"healthy","components":{"process":{"status":"up"}}}

It always answers 200.

Readiness. It checks the database and every worker. Use it for a readiness probe.

Status Body status Meaning
200 healthy Every component that decides readiness is up.
503 unhealthy At least one component that decides readiness is down.
{
"status": "healthy",
"components": {
"database": {"status": "up"},
"outbox-poller": {"status": "up"},
"inbox-relay": {"status": "up"},
"rabbitmq.orders-queue": {"status": "up"}
}
}
Component Present when Decides readiness
database always yes
outbox-capture outbox.capture.enabled is true no. The component shows a capture fault, and delivery continues through SQL.
outbox-poller always yes
retention-service retention.enabled is true yes
inbox-relay inbox.relay.enabled is true yes
rabbitmq.<source> one per RabbitMQ source yes
kafka.<source> one per Kafka source yes
nats.<source> one per NATS source yes

Each check has a bound of 3 seconds. A check that does not answer in time counts as down.

An alias of GET /health/ready, with the same status codes and body.

GET /metrics answers 200 with the Prometheus text format, content type text/plain; version=0.0.4; charset=utf-8. It needs no authentication. Keep it on an internal network, because the names and counts show destinations and traffic. The metrics page lists every metric.

The admin routes exist only when admin.enabled is true. Otherwise they answer 404. Each request needs the credentials of admin.auth, unless admin.insecure is true. The body is JSON of at most admin.maxPayloadBytes bytes. The route ignores an unknown field.

Evaluates a JSONata expression against a sample payload. It sends nothing and writes nothing.

Field Type Required Meaning
expression string yes The JSONata expression.
payload JSON yes The input of the expression.
mockTopic string no The value of $topic. Default: test.topic.
mockSource string no The value of $source.
timeoutMs integer no The evaluation timeout. Default: 100. QueueBox uses at most admin.maxTransformTimeoutMs.
Terminal window
curl -X POST http://localhost:9090/admin/transform/test \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"expression": "{ \"total\": $sum(items.(price * qty)) }",
"payload": {"items": [{"price": 10, "qty": 2}, {"price": 5, "qty": 3}]},
"mockTopic": "order.created"
}'
Status Body Meaning
200 {"success":true,"result":<json>,"context":{"messageId":"<uuid>","topic":"<topic>","attempt":0,"timestamp":"<iso-8601>"}} The expression ran. context shows the variables that it saw.
400 {"success":false,"error":"Invalid request: <reason>"} The body is not a valid request.
400 {"success":false,"error":"Invalid expression: <reason>"} The expression does not compile.
400 {"success":false,"error":"<reason>"} The evaluation failed or ran out of time.
401 {"success":false,"error":"<reason>"} The credentials are wrong or absent.
413 {"success":false,"error":"Request body exceeds <n> bytes"} The body is larger than admin.maxPayloadBytes.

Moves outbox rows back to pending, so the poller delivers them again. It takes only rows that are sent or dead. A row that waits or runs never moves. The move sets attempt to 0, scheduled_at to now, and clears last_error and the claim columns. The row keeps its id.

Field Type Meaning
ids array of UUIDs Only these rows.
topic string Only rows with this topic.
topics array of strings Only rows with one of these topics.
destination string Only rows whose topic the routes send to this destination. The first-match rule of the routes applies. With topics, only the topics in both sets.
createdAfter ISO-8601 instant Only rows created after this time.
createdBefore ISO-8601 instant Only rows created before this time.

Every field is optional, and the fields combine with AND. A request needs at least one field.

Terminal window
curl -X POST http://localhost:9090/admin/replay \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"destination": "orders-api", "createdAfter": "2026-09-01T00:00:00Z"}'
Status Body Meaning
200 {"moved":<n>} The number of rows that moved. QueueBox also logs the count and the filter.
400 {"error":"A replay needs at least one filter."} The request sets no field.
400 {"error":"Invalid request: <reason>"} The body is not a valid request.
401 {"error":"<reason>"} The credentials are wrong or absent.
413 {"error":"Request body exceeds <n> bytes"} The body is larger than admin.maxPayloadBytes.

Replay dead letters gives the procedure.