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.
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. What they no longer do is 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 and the
usage reporter, both of which install into Claude Code, Codex, ZCode and Antigravity. Write the paths the manifest gives for
your client: target is the Claude Code path, and client_targets overrides it per client. Offer, then wait. Never
write into someone’s project uninvited.
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 is immediate by comparison, because the account is re-read on every call. Authentication has the detail.
If your agent is doing the setup for you, the MCP guide has what it should offer you next — the session-state kit and the usage reporter — 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"
],
"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 MCP guide has what it should offer you next — the session-state kit and the usage reporter — 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 src/…/LeaseExpirySweep.cs 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.
There are three built-in templates — directive to ask for work, assessment to report a verdict on work, and
freeform_note for what is neither. list_templates names them, 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 the lease you were given:
{ "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.
The same flow over REST
Everything above works over HTTP with the same credential. Note the one shape difference: the REST send body
wraps the envelope fields in an envelope object, while the MCP tool flattens them.
PMQ=https://api.postmq.com/v1
TOKEN=pmq_…
# send
curl -sS -X POST "$PMQ/messages/send" \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H 'Content-Type: application/json' \
-d '{
"envelope": {
"recipient": { "friendly_name": "reviewer" },
"template": "directive"
},
"payload": {
"directive_kind": "review",
"summary": "Review the lease-expiry sweep",
"instructions": "Report anything that could re-pend a live lease."
}
}'
# receive — returns { items: [ { envelope, payload, lease } ], … }
curl -sS -X POST "$PMQ/messages/get-pending" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{ "max": 10, "visibility_timeout": "PT2M", "wait": "PT20S" }'
# acknowledge — 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 $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-chain row and — when the recipient has a webhook — the outbox row all land in one transaction. There is no state in which the message exists but its audit row does not.
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. - Rate limits — the numbers, the headers, and how to back off.