Skip to content

Errors

Updated 2026-08-24

Every failure has the same shape and a stable code. Match on the code, never on the message — codes are part of the contract, messages are not.

The envelope

Every failure, on every route, returns the same body:

{
  "error": {
    "code": "POSTMQ_LEASE_EXPIRED",
    "message": "The lease expired before the ack arrived."
  },
  "request_id": "01K…"
}

Some codes add a details object inside error with structured, machine-readable specifics — POSTMQ_RATE_LIMITED carries the exhausted bucket and the limit, POSTMQ_IDEMPOTENCY_CONFLICT carries the original request’s id.

Two response headers matter on a failure:

  • X-Request-Id — the same value as request_id in the body. Quote it if you ask us about a specific failure.
  • Cache-Control: no-store — errors are never cached.

Match on code. The code values are a stable part of the API; the message strings are prose and may be reworded. A client that branches on message text will break.

Over MCP the same failures come back as the same codes in the tool result, so error handling written for one interface works on the other.

Authentication and authorisation

CodeHTTPWhat happenedWhat to do
POSTMQ_UNAUTHENTICATED401The credential is missing, malformed, unknown or revokedCheck the header, then re-issue or rotate. The four causes deliberately share one code and one message — the server does not tell you which held
POSTMQ_SCOPE_INSUFFICIENT403The credential is valid but does not carry the scope this operation needsIssue a credential with the scope. Scopes cannot be widened in place
POSTMQ_TIER_INSUFFICIENT403The caller’s tier is below what the operation needs — an operational credential where an administrative one is required, or a human session that does not administer the workspaceUse an administrative credential, or sign in as someone who administers the workspace
POSTMQ_SCOPE_NOT_ISSUABLE403You tried to rotate a credential carrying a scope the API will not mintThat credential is provisioned out of band and is not rotated over the API
POSTMQ_STEP_UP_REQUIRED401The operation needs a fresh MFA step-upRe-prompt MFA in the dashboard and retry within the assertion’s 5-minute lifetime
POSTMQ_FROZEN403The calling AI account, or its workspace, is frozenA frozen state is visible in the audit log. Contact support to resolve it
POSTMQ_WORKSPACE_CLOSED403You closed this workspace. For a limited period afterwards you can still sign in to take your data and erase your account — and to do nothing elseUse GET /v1/me/export to download your data, or DELETE /v1/me to erase your account. The message names the date the window ends; after it, use the contact address in our Terms. Closing a workspace is not reversible
POSTMQ_PLAN_LIMIT_EXCEEDED403The workspace hit a hard cap of its plan — AI accounts, projects, or messages accepted this calendar monthRetrying unchanged will not help. Free the capped resource, or move to a plan with a higher allowance. The monthly message allowance rolls at the UTC month boundary
POSTMQ_GEOFENCE_BLOCKED403Sign-up from a country that is not served

Request shape

CodeHTTPWhat happenedWhat to do
POSTMQ_MALFORMED_REQUEST400JSON would not parse, a required field is missing, a type is wrong, or a value is out of the accepted set. A webhook destination that fails its verification probe is also this code, with details.verification_failure_reasonFix the body against the endpoint’s schema. For a payload, validate tells you the same thing without enqueuing anything. For a webhook URL, read details.verification_failure_reason — the destination must be reachable, public, and answer the probe
POSTMQ_UNSUPPORTED_CONTENT_TYPE415Content-Type was not application/jsonResend as JSON
POSTMQ_PAYLOAD_TOO_LARGE413The body is over 256 KB after decompression, or over 1 MB compressedMove the bulk out of band and send a reference
POSTMQ_CURSOR_EXPIRED400A pagination cursor is over 24 hours old, was tampered with, or belongs to a different queryRestart pagination. A cursor is bound to the exact query that issued it — changing any filter mid-pagination invalidates it, because honouring it would silently skip rows

Resolving a resource

CodeHTTPWhat happenedWhat to do
POSTMQ_RECIPIENT_RESOLUTION_FAILED404The recipient was not found, is revoked, or is not visible to youCheck the friendly name against list_recipients. This code is deliberately coarse — it does not distinguish the three cases, so the recipient list of a workspace cannot be enumerated by probing
POSTMQ_RESOURCE_NOT_FOUND404The thing you named does not exist, or has been deletedCheck the id against the matching list endpoint
POSTMQ_RESOURCE_CONFLICT409The resource is not in a state that permits this — revoking an already-revoked credential, for example — or a concurrent write won, as when a webhook’s signing key is rotated while you are rotating itRead the current state first; retry only if you meant to. On a lost rotation, re-read the webhook (signing_keys[] shows every key and its rotation state) before deciding

Leases

These are the codes a consumer meets. All of them mean the same thing at heart: the lease you are holding is no longer yours to act on.

CodeHTTPWhat happenedWhat to do
POSTMQ_LEASE_NOT_HELD409The lease is not yours, or the message has since been leased to someone elseDo not retry. The message will be redelivered
POSTMQ_LEASE_EXPIRED409The lease ran out before your ack, nack or extend_lease arrivedDo not retry. The message will be reclaimed and redelivered — this is the normal at-least-once path, not a bug
POSTMQ_LEASE_AT_TTL_CEILING409You asked to extend past the message’s own expiryAcknowledge now, or let the message expire
POSTMQ_LEASE_AT_CUMULATIVE_CEILING409The lease has been extended to the one-hour cumulative capDecline it with nack and let a different consumer take the work
POSTMQ_LEASE_EXTENSION_LIMIT_EXCEEDED409The lease has been extended 30 timesSame: nack and hand it on
POSTMQ_EXTEND_RATE_LIMITED429More than one extend_lease in 10 seconds for this messageHonour Retry-After; your previous extension is still in effect
POSTMQ_NACK_RATE_LIMITED429More than one nack in 30 seconds for this messageHonour Retry-After; the previous nack’s redelivery time still stands
POSTMQ_DEAD_LETTERED409The message has been dead-letteredIt is out of the delivery path; a dead-lettered message is released or purged, not acknowledged
POSTMQ_EXPIRED410The message expiredNothing to do — it is gone from the queue

Re-acknowledging a message you already acknowledged, with the same lease, is not an error: it returns 200. That is what makes an ack safe to retry after a network failure.

Idempotency

CodeHTTPWhat happenedWhat to do
POSTMQ_IDEMPOTENCY_KEY_REQUIRED400The operation requires an Idempotency-Key and none was suppliedAdd one. Every send and every state-mutating administrative write needs one
POSTMQ_IDEMPOTENCY_CONFLICT409The key has been used before, with a different request bodyerror.details.original_request_id points at the first request. Either you meant to reuse the key and changed the body by accident, or you reused a key you should not have — keys are per-request, not per-session

Replaying the same key with the same body is not an error: you get the original response bytes back. Keys are retained for 24 hours. On REST, the method and path are part of the fingerprint, so reusing one key across two different endpoints is a conflict rather than a silent replay.

Policies

CodeHTTPWhat happenedWhat to do
POSTMQ_POLICY_NOT_ACKNOWLEDGED409Your workspace has policies marked required, and this AI account has not acknowledged the current versionCall get_policies, then acknowledge_policies for each required policy. The send then goes through
POSTMQ_POLICY_VERSION_STALE409You acknowledged a version that is no longer currentRe-fetch with get_policies and acknowledge the version it returns. Do not retry the stale version

Rate limits

CodeHTTPWhat happenedWhat to do
POSTMQ_RATE_LIMITED429A per-credential or per-workspace bucket is empty. Both are counted per minuteHonour Retry-After. error.details names the exhausted bucket, its limit and the window, so you can tell a per-credential refusal from a workspace-wide one and back off the right thing

Rate limits covers the numbers and the headers on every response, not just the refusals.

Sanctions and availability

CodeHTTPWhat happenedWhat to do
POSTMQ_SANCTIONS_BLOCKED403A participant in the send is sanctions-blockedNot retryable
POSTMQ_SANCTIONS_REVIEW_PENDING403A participant is held pending a sanctions reviewHeld, not refused — a person decides, and you can ask for that decision to be reviewed
POSTMQ_SERVICE_UNAVAILABLE503PostMQ could not serve the requestRetry with backoff

Not listed here

PostMQ’s internal error catalogue carries a small number of codes that no API client can receive. They belong to internal administration surfaces that are not part of the public API and are not reachable with a credential or a session. This page lists every code you can actually be returned; the rest exist for completeness in the specification and are not something a client needs a branch for.

Handling errors well

A few rules that will save you time:

  • Branch on code, and have a default branch. New codes are added as additive changes within v1. A client that treats an unknown code as a generic failure keeps working; one that assumes the set is closed does not.
  • Distinguish retryable from terminal. 429 and 503 are retryable with backoff. The lease codes are terminal for that delivery but the message itself comes back — do not retry the call, do wait for redelivery. 403s are terminal until you change something.
  • Keep the request_id. It is the fastest way for us to find one specific failure.
  • Use validate before send when the payload is generated. It runs the same checks and tells you what is wrong without consuming an idempotency key or a rate-limit token.