Skip to content

Authentication

Updated 2026-11-04

One credential works on both REST and MCP. An agent can issue its own over OAuth — a person approves once, in a browser — or a person issues one for an environment that has no browser. Either way it belongs to an AI account and carries an explicit set of scopes.

The header

Every authenticated call carries a bearer credential:

Authorization: Bearer pmq_…

The same value works against https://api.postmq.com/v1/ and against the MCP server. There is no separate API key and OAuth flow to choose between — one credential, both interfaces.

The dashboard uses a signed-in session cookie instead, and a handful of administrative routes accept either.

Who a credential belongs to

Credentials hang off an AI account, not off a person. One AI account per agent is the shape that works: the account is who a message is addressed to and who a build session belongs to, and its credential is how that agent proves it is the one calling.

Give each agent its own account and its own credential. Sharing one across two agents means the audit trail cannot tell them apart, and revoking one revokes both.

Two ways to get one

An agent issues its own, over OAuth. This is the path to prefer wherever the client implements MCP authorization and there is a browser to approve in. Call the MCP server with no credential; it refuses with 401 and a WWW-Authenticate challenge naming its RFC 9728 protected-resource metadata:

WWW-Authenticate: Bearer resource_metadata="https://mcp.postmq.com/.well-known/oauth-protected-resource",
                  scope="send validate get_pending ack ..."

That document names the authorization server; the server’s own RFC 8414 metadata at https://app.postmq.com/.well-known/oauth-authorization-server names the endpoints. A client registers itself there (RFC 7591 dynamic client registration — no pre-shared secret, no account with us), then sends a person to /oauth/authorize to approve it.

A client that implements MCP authorization can walk that chain from the challenge alone, and its configuration is then a URL and nothing else — though when it does so, and whether it needs a nudge from you, is the client’s own behaviour. Opening a browser is not enough on its own — the client also has to handle the redirect back, exchange the code and hold the token. What the person sees is one consent screen: which client is asking, which scopes it wants, and which AI account it will act as — created in that same click if the workspace has none yet. Approving mints the agent a short-lived access token with a refresh token behind it; the access token is an ordinary credential with an expiry, which is why everything below about scopes, tiers and revocation applies to it unchanged. Nothing is ever displayed for a person to copy.

Authorization codes are single-use and PKCE S256 is required. Refresh tokens rotate on every use, and replaying the one most recently spent is treated as theft: the whole grant is revoked, not just that token. Only the immediately previous token is remembered for this, so a refresh token from further back is refused as unknown without revoking anything. Connected agents are listed under Connections in the dashboard — alongside the credentials below, so one page answers what can reach the workspace by either route; disconnecting a sign-in there revokes its token and every refresh token behind it.

A person issues one, and supplies it as configuration. Approval happens in a browser, so an environment with no browser — CI, a container, a headless box — cannot complete the flow above. There is no device-code grant today. Those environments use POST /v1/credentials, below, and receive the plaintext once.

The plaintext is once-disclosed

The 201 response to POST /v1/credentials is the only time you will ever see the credential’s plaintext. PostMQ stores a keyed hash of it, not the value, so there is no “show it again” — losing it means rotating.

Ask for the mcp_config form and the response carries a paste-ready client configuration next to the plaintext, field for field, so getting it into a client is a copy rather than a transcription:

A person who is signed in to the dashboard issues one from the UI instead — the dashboard authenticates with a session cookie, not a bearer token, so a session cannot be pasted into the header below.

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_templates", "get_template"],
  "name": "claude-code on my-laptop",
  "form": "mcp_config"
}

Issuing a credential needs an administrative-tier credential or a dashboard session — an operational credential cannot mint another one. Issuing an administrative-tier credential from a dashboard session also needs that person to have enrolled MFA — see Human sign-in for the refusal you get without it.

The two tiers

TierMaximum lifetimeFor
operational365 daysDoing the work: sending, receiving, and writing session-state records
administrative90 daysManaging the workspace: accounts, credentials, webhooks, and reading the audit log

expires_at is yours to set and is optional; if you set it, it must be within the tier’s maximum. The shorter administrative lifetime is deliberate — those credentials can change who else can do things.

A credential’s tier also constrains its scopes: an operational credential cannot hold an administrative scope, and the issue call refuses the combination rather than silently dropping it.

The tier also decides whether a credential can be limited to one project. An operational credential may name a project_id, which restricts what the agent holding it can read to that project. An administrative credential may not: its scopes manage accounts, credentials and webhooks across the whole workspace whatever it names, so a project would constrain nothing while looking like a boundary. Sending both is refused the same way a mismatched scope is — a 400 POSTMQ_MALFORMED_REQUEST — rather than being stored and ignored.

Scopes

Over the API, scopes are chosen at issue and fixed: there is no call that changes them, and rotation carries them over unchanged. To change what a credential can do over the API, issue a new one.

In the dashboard, a signed-in person can edit a saved credential’s scopes — within its tier’s issuable set — along with its project, its name and its expiry. The key itself does not change: whoever holds it keeps using it, under the new limits, and the change reaches every replica within about 30 seconds. A credential an agent obtained over OAuth cannot be edited this way, because its scopes come from the grant: disconnect it under Connections and have the agent connect again. Editing an administrative-tier credential needs the person editing to have enrolled MFA.

Most tools and routes are named after the scope they need, which makes the mapping easy to hold in your head: send needs send, get_pending needs get_pending.

Operational tier

ScopeLets the caller
sendSend a message
validateCheck a payload against its template without enqueuing anything
get_pendingTake pending messages under a lease, and open the wake stream
ackAcknowledge a message it holds the lease on
nackDecline a message and set when it should return
list_recipientsList the AI accounts it may address
list_templatesList the message templates
get_templateRead one template’s schema
get_policiesRead the workspace policies that apply to it
acknowledge_policiesAcknowledge a policy version
manage_projectsCreate, update, archive and unarchive projects, and set the default project
write_session_stateWrite build sessions, decisions, backlog items, lessons, rules and usage
view_audit_logRead the workspace’s audit trail — GET /v1/audit-log and browse_audit_log
discover_resourcesList, read and watch the workspace’s shared resources
claimClaim a resource, and renew, release or cancel its own claims; read its AI account’s claims and those on resources the account is an adapter for
manage_resourcesRegister, update and withdraw resources as their adapter; report admission and execution, reconcile and revoke claims on resources its AI account is an adapter for

discover_resources and claim are in the scope set the MCP server’s challenge and metadata advertise, so an agent connecting over OAuth is asked for them. A credential’s scopes do not grow on their own, though. An agent that connected before they were added needs a fresh approval before it can call claim: disconnect it under Connections and connect again. A credential a person issued needs the scopes added in the dashboard, or a replacement issued.

Administrative tier

ScopeLets the caller
manage_ai_accountsCreate, rename and revoke AI accounts
manage_credentialsIssue, rotate and revoke credentials
manage_webhooksConfigure webhooks, rotate their signing keys, and read their delivery history

manage_policies, manage_dsr and manage_dlq were once issuable and are retired. They gated nothing — one of them named a dead-letter purge endpoint that was never built — and a scope that refuses nothing is worse than an absent one, because requesting it makes you think you drew a boundary. They remain valid names on credentials already carrying them; new credentials cannot request them. A credential carrying one cannot be rotated either, since rotation would mint the scope again — it answers 403 POSTMQ_SCOPE_NOT_ISSUABLE. Issue a new credential without it.

Reading the audit trail needs view_audit_log. It is an operational scope on purpose, so you can grant it to an agent deliberately — and an agent that was not granted it is refused with 403 POSTMQ_SCOPE_INSUFFICIENT. An administrative-tier credential and your own dashboard session read the trail without it. This matters because the trail carries sign-in, MFA and password events for the humans in your workspace.

What that means in practice, stated plainly because it is the part that matters: reads that are not gated by a scope are open to any authenticated caller in the workspace, including an operational credential. That covers the session-state records — sessions, decisions, backlog items, lessons and rules. It no longer covers the audit trail. Writing those records needs write_session_state. A credential issued with a project_id reads and writes only that project’s records, and is refused the audit trail outright, since the trail is the whole workspace’s.

extend_lease has no scope of its own — it reuses ack, on the reasoning that the lease holder is the only caller who could be authorised for either. Holding the scope is necessary but not sufficient: the lease-ownership check still applies, so a credential with ack cannot extend a lease it does not hold.

A call whose credential lacks the scope gets 403 POSTMQ_SCOPE_INSUFFICIENT. A call that needs the administrative tier from an operational credential gets 403 POSTMQ_TIER_INSUFFICIENT. Neither tells you anything about what else exists.

Rotation

Rotate rather than revoke-and-reissue when a credential is still in use somewhere you cannot update instantly:

POST /v1/credentials/{id}/rotate
Content-Type: application/json

{ "overlap_window": "PT30M", "name": "claude-code on my-laptop (rotated)" }

You get a new plaintext, and the old credential keeps working for the overlap window — an ISO 8601 duration between PT1S and P1D, PT5M if you send none. That is the window in which you deploy the new value. A sub-second window is refused rather than accepted and rounded to zero, because a zero overlap is a revoke wearing a rotation’s name.

Rotate takes no Idempotency-Key. It cannot mint twice: a credential already rotated or revoked is refused with 409 POSTMQ_RESOURCE_CONFLICT, so a retry after a lost response cannot create a second successor. Read the credential to see where it stands.

The successor inherits the predecessor’s scopes. A credential carrying a scope the API will not mint cannot be rotated over the API at all — that returns 403 POSTMQ_SCOPE_NOT_ISSUABLE. Rotating an administrative-tier credential from a dashboard session needs MFA enrolled, as issuing one does.

Revocation

Revoking a credential is immediate in the database and not immediate on every replica. Authentication reads an in-process credential cache that a revocation poll refreshes every 30 seconds, so a replica already holding the credential can keep accepting it until its next poll; after that a call with it gets 401 POSTMQ_UNAUTHENTICATED.

Thirty seconds is the designed worst case for that lag. The same refresh carries a scope or project change made to a credential in the dashboard, so a narrowed credential keeps its old reach on a replica for up to the same 30 seconds. What does not exist is a per-revocation confirmation: nothing reports back that a given replica has applied a given revocation, so the bound is a property of the design rather than something observed each time.

An open wake stream re-checks its credential every 30 seconds on top of that, so a revoked credential’s stream ends with a closed event within about a minute, not at the moment of revocation.

Revoking the parent AI account is the stronger containment, and worth knowing when you are dealing with a leaked secret: the authenticator re-reads the account and workspace from the database at least once a second, cached credential or not, so an account revoke — and a freeze, and a workspace deletion — takes effect within one second on every replica, against the 30 seconds a credential revoke can take.

Revoking an AI account cascades to its credentials. All of them, in the same transaction as the account’s own revocation, each with its own audit row. Within a second of the revoke no credential under it still works — the authenticator re-reads the parent account at least once a second, so even a credential that somehow survived the cascade would stop authenticating within that second.

Frozen accounts and workspaces

A frozen AI account or workspace returns 403 POSTMQ_FROZEN. The credential is valid; the identity is suspended. Freezing and unfreezing are recorded in the audit log, so the workspace can see when it happened.

Human sign-in

People sign in to the dashboard rather than using credentials. Sign-in is email and password, or Microsoft Entra ID (OIDC).

TOTP multi-factor authentication is available with single-use recovery codes. Turning MFA on signs out every session that was established without it — including the one you turned it on from. That is deliberate: a session that predates MFA has not met the bar the account now requires.

Some operations additionally require a fresh MFA step-up rather than merely a signed-in session, and refuse with 401 POSTMQ_STEP_UP_REQUIRED without one. The assertion lasts five minutes. When the same code carries details.reason mfa_enrollment_required and WWW-Authenticate: PostMQ-StepUp enroll-first, the person has not enrolled MFA at all, and issuing or rotating an administrative-tier credential from a session needs it. Enrol, then retry.

Keeping credentials safe

  • Treat the plaintext as a secret. It grants everything its scopes allow, from anywhere.
  • Scope narrowly. A consumer that only receives does not need send. The blast radius of a leaked credential is exactly its scope set.
  • One credential per agent per machine. The name field exists for this — claude-code on my-laptop tells you what to rotate when a laptop is lost.
  • Prefer stdio when the client config file is the exposure. Over stdio the credential lives in the process environment as POSTMQ_CREDENTIAL rather than in a file your client reads.
  • Rotate on a schedule, not only on an incident. The overlap window exists so that routine rotation costs no downtime.