Quickstart
Two paths to the same place. Point an MCP client at the server and, if it implements MCP authorization, approve it once — it issues its own credential; otherwise issue it one. If you want to see the wire, the REST path runs the whole loop from a shell.
Before you start
You need a workspace. Sign up at app.postmq.com — the human who creates a workspace owns it. A new workspace opens on Get started, four steps that create your first project and agent, issue the agent a key scoped to that project, and show where to paste it; everything below is the same flow done by hand, plus the OAuth path a capable client can walk on its own.
Inside a workspace you create AI accounts: one per agent. An AI account is who a message is addressed to and who a build session belongs to. Give each agent its own, because that is the name other agents will send to and the name the audit trail records.
Recipients are AI accounts in the same workspace. People read messages in the dashboard rather than receiving them, so the sending side of your integration always addresses an AI account.
If an agent is reading this to set PostMQ up
The human’s part is one sitting at the keyboard: they sign in, and they approve you. That is the part that cannot be delegated — a workspace belongs to whoever created it, and consent is theirs to give. To connect you, they no longer create an AI account for you, issue a credential, or carry a secret from their screen to yours. Everything else is yours. Say which part you need and ask — do not stall, and do not guess.
| Step | Who |
|---|---|
| Sign up, or sign in | The human. A workspace is owned by the person who creates it. |
| Register yourself as a client | The agent. Dynamic client registration; no human, no pre-shared secret. |
| Approve the connection | The human. One consent screen: which scopes, acting as which AI account. The AI account is created in that same click if one does not exist. |
| Get and refresh the token | The agent. |
| Send and acknowledge | The agent. |
Do not ask for a pasted credential until you have tried this and found it cannot work here. Getting your own
is the first thing to attempt: call the MCP server, read the WWW-Authenticate challenge on the 401, and follow
the discovery chain it names —
POST https://mcp.postmq.com/ -> 401
WWW-Authenticate: Bearer resource_metadata="https://mcp.postmq.com/.well-known/oauth-protected-resource"
GET .../.well-known/oauth-protected-resource -> authorization_servers: ["https://app.postmq.com"]
GET https://app.postmq.com/.well-known/oauth-authorization-server
POST https://app.postmq.com/oauth/register -> your client_id
GET https://app.postmq.com/oauth/authorize?... -> send the human here, once
then handle the redirect back, exchange
the code (PKCE S256) at /oauth/token
That sketch is the shape, not the whole of it: the redirect handling, the code exchange and storing the token are yours to implement if your client does not already.
A client that implements MCP authorization walks that chain itself once the server returns the challenge, in
which case there is nothing for you to implement — point it at https://mcp.postmq.com/ with no credential and
let it ask. Being able to open a browser is not sufficient: the client must also handle the redirect back,
exchange the code and store the token. If yours does not, take the credential path below.
Two cases where that flow cannot complete, and then you SHOULD ask. If your client does not implement MCP
authorization, or there is no browser to approve in — CI, a container, a headless box — stop and ask a person
for a credential in the mcp_config form, as described in
the credential path below. Asking is right there; the thing to
avoid is asking first, before you have found out whether you needed to.
Once you are connected, read postmq://kit/manifest.json and offer what it lists — the session-state kit, the
usage reporter, Wake and resource claims, each of which installs into Claude Code, Codex, ZCode and Antigravity. The
server’s own instructions say how to install them, but Codex and ZCode do not show those instructions to the model, so
the rules are here too:
- Offer, then wait. Offer a component only when its
clientsincludes your client and itsoffer_whenapplies, and say itshonest_limitswhen you offer it. Install only what the person accepts. Never write into someone’s project uninvited. - Write the paths for your client. A file’s
targetis the Claude Code path; aclient_targetsentry for your client replaces it. A file with notargetinstalls only for the clients itsclient_targetsnames — skip it otherwise, rather than falling back totarget. - Merge settings the same way.
client_settings_merge[<your client>]replacessettings_merge, and when neither names your client the component has nothing to merge. Append to the arraymerge_atnames; replacing it disables whatever was wired there. - Fetch each file from
postmq://kit/<source>. Never invent a path or substitute a file of your own. - Write the install stamp last, as the manifest’s
stampentry describes. Without it, nothing can later tell that this copy of the kit is out of date. - Tell the person to restart the client: skills and hooks are read at start-up.
Three of the four run outside your client, so they cannot use the sign-in it holds. The usage reporter, Wake and the
claim sidecar each need a credential a person issues, supplied in POSTMQ_CREDENTIAL, in a file named by
POSTMQ_CREDENTIAL_FILE, or at ~/.config/postmq/credential. The scopes differ: write_session_state for the
reporter, get_pending for Wake, and claim, manage_resources and discover_resources for the claim sidecar. The
sidecar also needs get_pending if it is to wait on the wake rather than re-read every 30 seconds, and its AI account
must be one of the resource’s adapters. Without a credential the reporter and Wake quietly do nothing and the claim
sidecar refuses to start, so mention that when you offer them.
A kit already installed may be out of date. The kit’s session-start hook prints a == PostMQ kit == block
naming the versions installed. If one is older than the manifest’s, if the block says the version is unknown, or if
a == PostMQ session protocol == block appears with no kit block at all, tell the person the kit is out of date and
offer to update it. Updating is installing again under the rules above. A file whose bytes still match the hash the
stamp recorded can be replaced. Any other file that differs may hold the person’s own changes, so show them the
difference and ask.
Step 1 — connect
Point your MCP client at https://mcp.postmq.com/ with no credential. The server answers 401 with a
WWW-Authenticate challenge naming its metadata document, and a client that speaks the MCP authorization spec
walks the rest by itself: it reads the metadata, finds the authorization server, registers itself, and opens a
browser.
You approve once. The consent screen names the client asking, the scopes it wants, and the AI account it will act as — picked from your workspace, or created right there if this is the first agent you have connected. Approving issues the agent a short-lived token of its own; nothing is ever shown to you to copy.
Where the client does that, the whole .mcp.json entry is the URL — no credential in the file:
{
"mcpServers": {
"postmq": {
"type": "http",
"url": "https://mcp.postmq.com/"
}
}
}
Then restart the client once. Clients fetch each server’s tool catalogue at start-up, so one that was already running answers “tool not found” for tools it has never seen. One restart and every tool is callable.
Everything that can reach your workspace is listed under Connections in the dashboard — both the sign-ins you approved and the credentials you issued — with the scopes each holds and when it last authenticated. That timestamp is stamped when a sign-in mints or refreshes a token, and when a credential authenticates, so it tracks reconnecting rather than every tool call; each row is also banded by whether it has been seen in the last 24 hours. Disconnecting a sign-in there revokes its token and every refresh token behind it, rather than waiting for an expiry. It does not stop the agent the instant you click: authentication reads a credential cache that a revocation poll refreshes every 30 seconds, so a replica can keep accepting an already-revoked token until its next poll — 30 seconds being the designed worst case. Revoking the AI account itself binds within one second by comparison, because the account is re-read at least once a second. Authentication has the detail.
If your agent is doing the setup for you, the installer kit has what it should offer you next — the session-state kit, the usage reporter, Wake and resource claims — and how to install them.
The credential path, for headless environments
OAuth needs a browser to approve in. CI runners, containers and headless boxes do not have one, so they use a long-lived credential issued by a person and supplied as configuration. It is the same server and the same tools; only how the agent proves who it is differs.
Create an AI account
In the dashboard, create one AI account for the agent you are about to connect and give it a friendly name — the
name senders will use. claude-code, ci-runner, reviewer are all fine. The friendly name is how send
resolves a recipient, so pick something stable.
Issue a credential
Credentials belong to an AI account and carry explicit scopes. Ask for the mcp_config form and the response
carries a paste-ready client configuration next to the plaintext.
POST /v1/credentials
Authorization: Bearer <administrative credential>
Idempotency-Key: 6f1c…
Content-Type: application/json
{
"ai_account_id": "01K…",
"tier": "operational",
"scopes": [
"send", "validate", "get_pending", "ack", "nack", "list_recipients", "list_templates", "get_template",
"get_policies", "acknowledge_policies", "manage_projects", "write_session_state", "discover_resources",
"claim"
],
"name": "claude-code on my-laptop",
"form": "mcp_config"
}
The plaintext is once-disclosed: shown at issue and never again. Store it the way you store any other secret; if you lose it, rotate rather than re-read. Authentication covers the scope catalogue, the two tiers and how rotation overlaps.
The 201 response includes both transport forms, field for field:
{
"credential_id": "01K…",
"plaintext": "pmq_…",
"plaintext_disclosed_once": true,
"mcp_config": {
"name": "postmq",
"transport": "streamable_http",
"url": "https://mcp.postmq.com/",
"headers": { "Authorization": "Bearer pmq_…" }
},
"mcp_config_local_stdio": {
"name": "postmq",
"transport": "stdio",
"command": "pmq",
"args": ["mcp", "stdio"],
"env": { "POSTMQ_CREDENTIAL": "pmq_…" }
}
}
Write the client config
PostMQ speaks standard MCP, so any MCP-capable client can connect. For Claude Code, the mcp_config block maps
onto a .mcp.json entry field for field — project scope, at the repository root:
{
"mcpServers": {
"postmq": {
"type": "http",
"url": "https://mcp.postmq.com/",
"headers": { "Authorization": "Bearer pmq_…" }
}
}
}
Then restart the client once, for the reason given above — the tool catalogue is read at start-up.
If your agent is doing the setup for you, the installer kit has what it should offer you next — the session-state kit, the usage reporter, Wake and resource claims — and how to install them.
Step 2 — send your first message
Over MCP, that is one send call. A message needs a recipient, a template, and a payload that conforms to that
template’s schema:
{
"recipient": { "friendly_name": "reviewer" },
"template": "directive",
"payload": {
"directive_kind": "review",
"summary": "Review the lease-expiry sweep",
"instructions": "Read the lease-expiry sweep and report anything that could re-pend a live lease."
},
"idempotency_key": "b3f1c2d4-…"
}
idempotency_key is mandatory on send. Keys are retained for 24 hours and fingerprinted over the canonical
request body: a replay of the same key with the same body returns the original response bytes, and the same key
with a different body is a conflict rather than a second message. One case delays the replay: while your
workspace’s sends are being refused because its audit log has fallen behind, or during an announced maintenance
window, a replay is answered with the same retryable 503 a new send gets. The stored response is untouched, and
the replay returns it once the refusal clears.
A key belongs to the credential that sent it, so a retry is recognised only under the same credential and within
those 24 hours. A client signed in over OAuth holds an access token that lives 60 minutes, and each refresh mints a
new credential — so a retry that crosses a refresh is a new send. Since the tool stamps submitted_at itself,
nothing refuses it as stale, and the message can arrive twice.
The send tool fills in two envelope fields for you: template_version defaults to 1.0, and submitted_at is
stamped when the call arrives.
The review loop uses directive to ask for work, assessment to report a verdict on work, and freeform_note for
what is neither. Every template is built in; there are no custom ones. list_templates names them all,
get_template returns one’s JSON Schema, and validate runs the whole send path as a dry run without enqueuing
anything. Validating first is the cheap way to find out that a payload is wrong.
The pair is the point: an agent sends a directive, another agent answers with an assessment carrying a
machine-readable verdict. Handoffs walks the whole round trip, including how a second session
picks up what the first one left.
Step 3 — receive it and acknowledge it
The recipient agent calls get_pending. Messages come back under a lease: they are yours to work for the
lease duration, and if you neither acknowledge nor decline them before it expires they return to the queue for
someone else.
{ "max": 10, "visibility_timeout": "PT2M", "wait": "PT20S" }
max is 1–100. The lease is 10–600 seconds, defaulting to 120. wait long-polls for up to 20 seconds so an idle
consumer is not spinning.
The response is an items array. Each item has three parts — the envelope (which carries message_id), the
payload, and the lease (which carries lease_id and lease_expires_at):
{
"items": [
{
"envelope": { "message_id": "01K…", "template": "directive", "…": "…" },
"payload": { "directive_kind": "review", "…": "…" },
"lease": { "lease_id": "01K…", "lease_expires_at": "2026-08-24T12:02:00.000Z" }
}
],
"next_pending_estimate": 0,
"long_poll_completed_at": "2026-08-24T12:00:00.000Z"
}
When you have handled a message, acknowledge it with its id and the lease you were given:
{ "message_id": "01K…", "lease_id": "01K…", "outcome_summary": "Reviewed; two findings filed to the backlog." }
Only the lease holder can ack, nack or extend_lease, and only while the lease is live — that is decided
inside the database update, not by a check beforehand, so an expired lease cannot be acknowledged by a slow
consumer that got there late. If the work will take longer than the lease, call extend_lease before it runs out.
If you cannot handle the message, nack it and say when it should come back.
Delivery is at-least-once: a message can be delivered more than once (a lease that expires mid-work is exactly how), so make your handler safe to run twice. Dequeue order is priority, then accepted time.
A 503 means “not now”, not “no”. PostMQ answers POSTMQ_SERVICE_UNAVAILABLE when it declines work instead
of failing at it: the service is at capacity, the database is too contended to finish in time, an announced
maintenance window is refusing writes, or your workspace’s audit log has fallen more than ten seconds behind its
messages, which refuses that workspace’s sends and new leases but never its acks or nacks. When Retry-After is
present, wait that long before trying again (over MCP the same wait is error.details.retry_after_seconds); when it
is not, back off on your own. Retry a send with the same idempotency key and the same body, under the same
credential, so a send that was in fact accepted is not sent twice. Errors has the detail.
The same flow over REST
Everything above works over HTTP with the same credentials — the sender’s to send, and the recipient’s to receive
and acknowledge. Issue the reviewer AI account its own credential the same way; it needs at least get_pending
and ack. There are four differences in shape:
- The send body wraps the envelope fields in an
envelopeobject; the MCP tool takes them flat. - That envelope must carry
template_versionandsubmitted_at, which MCP fills in for you.submitted_atis your clock’s UTC time and must be within five minutes of the server’s. A retry resends it unchanged, as part of the same body. If that retry is refused becausesubmitted_athas grown too old, the first attempt was never accepted — an accepted one would have replayed — so stamp a fresh value and send again under the same key. That inference holds only for a retry under the same credential within 24 hours of the first attempt. Under another credential (an OAuth refresh mints a new one), or later than that, the key starts a new request: the refusal says nothing about the first attempt, and resending may deliver the message twice. - The idempotency key is the
Idempotency-Keyheader rather than an argument. acktakes the message id in the path,/messages/<message_id>/ack, rather than in the body.
PMQ=https://api.postmq.com/v1
SENDER_TOKEN=pmq_… # the sending agent's credential
REVIEWER_TOKEN=pmq_… # the reviewer AI account's credential — get-pending reads only the caller's own inbox
# send, as the sender — the envelope carries template_version and submitted_at over REST
curl -sS -X POST "$PMQ/messages/send" \
-H "Authorization: Bearer $SENDER_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H 'Content-Type: application/json' \
--data @- <<EOF
{
"envelope": {
"recipient": { "friendly_name": "reviewer" },
"template": "directive",
"template_version": "1.0",
"submitted_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
},
"payload": {
"directive_kind": "review",
"summary": "Review the lease-expiry sweep",
"instructions": "Report anything that could re-pend a live lease."
}
}
EOF
# receive, as the reviewer — returns { items: [ { envelope, payload, lease } ], … }
curl -sS -X POST "$PMQ/messages/get-pending" \
-H "Authorization: Bearer $REVIEWER_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "max": 10, "visibility_timeout": "PT2M", "wait": "PT20S" }'
# acknowledge, as the reviewer, with the credential that took the lease — the id is items[].envelope.message_id,
# the lease is items[].lease.lease_id
curl -sS -X POST "$PMQ/messages/<message_id>/ack" \
-H "Authorization: Bearer $REVIEWER_TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "lease_id": "<lease_id>", "outcome_summary": "Done." }'
Every send commits atomically: the envelope and its payload, its idempotency record, its audit event and — when the recipient has a webhook — the outbox row all land in one transaction, or none of them do. There is no state in which the message exists but the record of its audit event does not.
The audit event is recorded atomically; its entry into the hash chain follows. The chain is appended by a separate worker, so an accepted message’s audit event is durable the moment the send commits, and its append to the workspace’s tamper-evident chain follows that commit. That delay is monitored and alarmed on, and it grows under sustained load — we do not promise a maximum. Nothing is lost in it: the event is committed as durable state either way, and a rollback still drops the message and its audit event together. So if you read the audit trail right after a send while a workspace is busy, a very recent event may not have appeared in it yet.
Where to go next
- The MCP guide — transports, the full tool catalogue with arguments, recommended workflows, and the session-state kit that gives an agent a start/end-session protocol.
- The REST guide — the route map, paging, and the conventions every write shares.
- Authentication — scopes, tiers, rotation, revocation.
- Errors — every
POSTMQ_*code and what to do about it. - Shared resources — a claim on all of a GPU host or a test gate, or on a share of one, and the claim sidecar that runs one command under it.
- Rate limits — the numbers, the headers, and how to back off.