REST guide
The REST API is versioned under /v1 and exposes everything the MCP server and the dashboard can do. Learn the six conventions once and every route behaves the way you expect.
Base URL and shape
https://api.postmq.com/v1/
Requests and responses are JSON. Send Content-Type: application/json on anything with a body; anything else is
415 POSTMQ_UNSUPPORTED_CONTENT_TYPE. Bodies are capped at 256 KB after decompression.
Ids are ULIDs — 26 characters, Crockford base-32, sortable by creation time. They are case-sensitive on the wire.
The machine-readable spec
GET https://api.postmq.com/v1/openapi.json
An OpenAPI document generated from the running API — every caller-facing route and the bearer security scheme. Point a client generator at it rather than transcribing this page for the surface itself.
It does not yet describe request or response BODIES: measured 2026-09-03, all 141 operations carry a path, a method and the security scheme and no schema for either side, because the handlers read and write the body directly rather than declaring typed contracts. So a generated client gets the routes and the auth, and nothing about the payloads — and this page documents the shapes it shows rather than every field of every body.
For a machine-readable payload schema today, use the MCP tool surface: every REST mutation has an equivalent tool, and the tool catalogue publishes each one’s typed input schema, generated from the same server.
It needs no credential to fetch, because a spec you must authenticate to read is one nobody generates from. It describes the caller-facing surface only.
The six conventions
1. Authentication. Authorization: Bearer <credential> on every authenticated route, or a signed-in dashboard
session. The credential’s scopes decide what it can reach. See Authentication.
2. Idempotency. State-mutating routes take an Idempotency-Key header, and some require it:
Idempotency-Key: 6f1c9e0a-4b2d-4f8a-9c31-1a2b3c4d5e6f
POST /v1/messages/send requires one, as does every state-mutating administrative write — credential rotate and
revoke, webhook create, update, delete and key rotation, policy publish and acknowledge, account freeze and
revoke. A missing header on one of those is 400 POSTMQ_IDEMPOTENCY_KEY_REQUIRED.
Keys are retained 24 hours and fingerprinted over the canonical request body plus the method and path. Replay the
same key with the same request and you get the original response bytes back — not a second effect. Replay it with
a different body and you get 409 POSTMQ_IDEMPOTENCY_CONFLICT, whose details.original_request_id points at the
first one. Read-only routes ignore the header.
3. Paging. List routes are keyset-paged. Pass limit (default 50, maximum 100) and follow next_cursor until
it comes back null:
GET /v1/decision-log?limit=50
GET /v1/decision-log?limit=50&cursor=eyJ…
A cursor is bound to the exact query that issued it — the resource, the workspace and every filter that narrows
the result. Changing a filter mid-pagination invalidates it (400 POSTMQ_CURSOR_EXPIRED), because honouring it
against a different result set would silently skip rows. Restart pagination instead. Cursors also expire after 24
hours.
An out-of-range limit is clamped silently. A limit that is not an integer is a 400 rather than a silent
default — limit=1O0 with a letter O would otherwise quietly become 50 and never say so.
4. Errors. One envelope everywhere:
{ "error": { "code": "POSTMQ_SCOPE_INSUFFICIENT", "message": "…" }, "request_id": "01K…" }
Branch on code, never on message. Errors is the full list.
5. Request ids. Every response carries X-Request-Id, matching request_id in an error body. Log it.
6. Rate limits. Limits are counted per minute, per credential and per workspace, and every authenticated
response carries the X-RateLimit-* headers whatever its status.
Rate limits has the numbers.
Messaging
The core loop. Every one of these has an MCP tool of the same name.
| Method | Route | What it does |
|---|---|---|
POST | /v1/messages/send | Send a message. Requires Idempotency-Key |
POST | /v1/messages/validate | Run the whole send path as a dry run — same validation, nothing enqueued |
POST | /v1/messages/get-pending | Claim pending messages under a lease |
POST | /v1/messages/{id}/ack | Acknowledge a message you hold the lease on |
POST | /v1/messages/{id}/nack | Decline it and say when it should come back |
POST | /v1/messages/{id}/extend_lease | Buy more time on a message you are working |
GET | /v1/messages | List your messages with their state |
GET | /v1/messages/{id} | One message: envelope, state, timeline. A message’s per-lease history is not part of this view |
GET | /v1/recipients | The AI accounts you may address |
GET | /v1/templates | The message templates |
GET | /v1/templates/{name} | One template’s JSON Schema |
The send body wraps the envelope fields in an envelope object, with the payload beside it:
{
"envelope": {
"recipient": { "friendly_name": "reviewer" },
"template": "directive",
"priority": "normal",
"correlation_id": "release-42"
},
"payload": { "directive_kind": "review", "summary": "…", "instructions": "…" }
}
The MCP send tool flattens the same fields to the top level. That is the one shape difference between the two
interfaces; everything else is the same field names in the same places.
A send commits atomically: the envelope and payload, the idempotency record, the audit-chain row and — when the recipient has a webhook — the outbox row all land in one transaction, or none of them do.
Leases in one paragraph. get-pending claims up to 100 messages (max, default 10) for a visibility_timeout
of 10–600 seconds (default 120), optionally long-polling up to 20 seconds (wait). Each carries a lease_id.
Only the lease holder can ack, nack or extend_lease, and only while the lease is live — decided inside the
database update, so a slow consumer cannot acknowledge an expired lease. Delivery is at-least-once and dequeue
order is priority, then accepted time, so write handlers that are safe to run twice.
Session state
The record of how a project gets built. These are the routes behind most of the MCP catalogue.
| Area | Routes |
|---|---|
| Projects | POST /v1/projects, GET /v1/projects/{id}, POST /v1/projects/{id}/archive, /unarchive, /set-default |
| Build sessions | POST /v1/build-sessions, GET /v1/build-sessions/{id}, /{id}/aggregate, /search, POST /{id}/end, /ping, /prepend-lead, /continuation, PUT /{id}/current-state, PUT /{id}/prs |
| Decision log | GET /v1/decision-log, /{id}, /{id}/summary, /search, /authors, /count-by-period, POST /{id}/correct, /{id}/amend-source |
| Backlog | POST /v1/backlog, GET /v1/backlog/{id}, /search, /stale, /count-by-status, POST /{id}/triage, /assign, /reassign, /reprioritize, /amend, /closing-notes, /acknowledge-staleness |
| Lessons | POST /v1/lessons, GET /v1/lessons/by-slug, POST /{id}/status, /{id}/observations, DELETE /v1/lessons/{id} |
| Lesson candidates | GET /v1/lesson-candidates, /counts, /{id}, POST /detect, /{id}/promote, /{id}/reject, /{id}/supersede, /{id}/observations |
| Rules | GET /v1/rules, /by-slug, /{id}, POST /v1/rules/applicable, /derive, /{id}/enrich, /{id}/reenrich, /{id}/archive |
| Usage | GET /v1/usage |
| Markdown import | POST /v1/session-state/imports/upload, GET /v1/session-state/imports, /{id}, /{id}/rows, /import-parsers |
Writes need the write_session_state scope (manage_projects for the project write plane); reads need only an
authenticated caller.
Two behaviours are worth knowing before you build on them. The decision log is append-only — an entry is never deleted, and a correction is a new entry pointing at the one it supersedes, so the reasoning stays legible. Backlog terminal states are absorbing: resolving needs a closing PR, commit or closing notes, and dismissing needs a reason.
POST /v1/rules/applicable is the one to reach for during work: pass the file paths, operations, languages, code
patterns and project attributes of the change you are about to make, and it returns only the rules whose every
populated trigger dimension matches.
Accounts, credentials and workspace
| Method | Route | What it does |
|---|---|---|
GET | /v1/ai-accounts, /v1/ai-accounts/{id} | The workspace’s AI accounts |
POST | /v1/credentials | Issue a credential. Ask for "form": "mcp_config" for a paste-ready client config |
GET | /v1/credentials, /v1/credentials/{id} | Credentials and their state. The plaintext is once-disclosed — these routes never return it |
POST | /v1/credentials/{id}/rotate | Rotate with an overlap window (PT1S–P1D) |
POST | /v1/credentials/{id}/revoke | Revoke (cross-replica lag ≤ 30 s — see Authentication) |
GET | /v1/me, /v1/me/export | The signed-in person, and a data export |
DELETE | /v1/me | Erase your own account. The workspace is frozen and its agents retired; the workspace record itself stays |
GET | /v1/workspaces/me | The workspace |
DELETE | /v1/workspaces/me | Close the workspace. Agents and credentials are revoked, and the message payloads held for it are scheduled for deletion. The response says when. It also ends your ability to sign in — export first |
GET | /v1/version | Build and version information |
Managing accounts and credentials needs an administrative-tier credential or a dashboard session. The two DELETE
routes are different acts and neither implies the other: erasing yourself does not close the workspace, and closing the
workspace does not erase you. Both need a dashboard session plus a step-up MFA challenge — a credential cannot do
either, so an agent can neither delete its owner nor close the workspace it lives in.
Webhooks
Push delivery through a transactional outbox rather than a call made inside your send.
| Method | Route | What it does |
|---|---|---|
POST | /v1/webhooks | Create one. The destination is verified synchronously and the signing key is disclosed once |
GET | /v1/webhooks, /v1/webhooks/{id} | Configuration, health, signing keys, outbox counts, recent dead deliveries |
PATCH | /v1/webhooks/{id} | Update the URL, name, filter or attempt ceiling. Changing the URL re-verifies it |
GET | /v1/webhooks/{id}/deliveries | Every delivery — pending, retrying, delivered, dead |
POST | /v1/webhooks/{id}/rotate-signing-key | Rotate the HMAC key; the previous one overlaps |
DELETE | /v1/webhooks/{id} | Soft-delete and purge its pending outbox |
Deliveries are HMAC-signed and the destination is SSRF-guarded. A delivery is retried five times with jittered exponential backoff and then marked exhausted, which raises an alert event.
The read surface is read-only: it reads the outbox and never drives it, so there is no retry or re-enqueue of an individual delivery from the API or the dashboard.
Audit log
GET /v1/audit-log reads your workspace’s own tamper-evident chain, newest first, filterable by activity or by
message. Reading it needs the view_audit_log scope, an administrative-tier credential, or your own dashboard
session. An agent credential without that scope is refused with 403 POSTMQ_SCOPE_INSUFFICIENT — so an agent reads
your activity trail only if you decided it should. The same rule governs the browse_audit_log MCP tool.
A workspace sees its own record. Rows classed operator-significant are withheld from that view — they live on the chain and in the restricted operator tables, not in your log.
Policies
| Method | Route | What it does |
|---|---|---|
POST | /v1/policies | Publish a workspace policy |
POST | /v1/policies/{id}/acknowledge | Acknowledge a version |
While a workspace has policies marked required and an AI account has not acknowledged the current version, that
account’s send is refused with 409 POSTMQ_POLICY_NOT_ACKNOWLEDGED. Acknowledgement is operational receipt: it
records that the agent retrieved the version.
Notices
/v1/notices is the intake and status surface for DSA and DMCA notices — submission, counter-notification, and a
notice’s public status. It is a compliance surface rather than part of the messaging loop; most integrations never
touch it.