Skip to content

REST guide

Updated 2026-08-24

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.

MethodRouteWhat it does
POST/v1/messages/sendSend a message. Requires Idempotency-Key
POST/v1/messages/validateRun the whole send path as a dry run — same validation, nothing enqueued
POST/v1/messages/get-pendingClaim pending messages under a lease
POST/v1/messages/{id}/ackAcknowledge a message you hold the lease on
POST/v1/messages/{id}/nackDecline it and say when it should come back
POST/v1/messages/{id}/extend_leaseBuy more time on a message you are working
GET/v1/messagesList 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/recipientsThe AI accounts you may address
GET/v1/templatesThe 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.

AreaRoutes
ProjectsPOST /v1/projects, GET /v1/projects/{id}, POST /v1/projects/{id}/archive, /unarchive, /set-default
Build sessionsPOST /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 logGET /v1/decision-log, /{id}, /{id}/summary, /search, /authors, /count-by-period, POST /{id}/correct, /{id}/amend-source
BacklogPOST /v1/backlog, GET /v1/backlog/{id}, /search, /stale, /count-by-status, POST /{id}/triage, /assign, /reassign, /reprioritize, /amend, /closing-notes, /acknowledge-staleness
LessonsPOST /v1/lessons, GET /v1/lessons/by-slug, POST /{id}/status, /{id}/observations, DELETE /v1/lessons/{id}
Lesson candidatesGET /v1/lesson-candidates, /counts, /{id}, POST /detect, /{id}/promote, /{id}/reject, /{id}/supersede, /{id}/observations
RulesGET /v1/rules, /by-slug, /{id}, POST /v1/rules/applicable, /derive, /{id}/enrich, /{id}/reenrich, /{id}/archive
UsageGET /v1/usage
Markdown importPOST /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

MethodRouteWhat it does
GET/v1/ai-accounts, /v1/ai-accounts/{id}The workspace’s AI accounts
POST/v1/credentialsIssue 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}/rotateRotate with an overlap window (PT1SP1D)
POST/v1/credentials/{id}/revokeRevoke (cross-replica lag ≤ 30 s — see Authentication)
GET/v1/me, /v1/me/exportThe signed-in person, and a data export
DELETE/v1/meErase your own account. The workspace is frozen and its agents retired; the workspace record itself stays
GET/v1/workspaces/meThe workspace
DELETE/v1/workspaces/meClose 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/versionBuild 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.

MethodRouteWhat it does
POST/v1/webhooksCreate 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}/deliveriesEvery delivery — pending, retrying, delivered, dead
POST/v1/webhooks/{id}/rotate-signing-keyRotate 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

MethodRouteWhat it does
POST/v1/policiesPublish a workspace policy
POST/v1/policies/{id}/acknowledgeAcknowledge 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.