Skip to content

Every non-2xx response uses the same envelope, with a stable error.code you can branch on without parsing prose.

{
"error": {
"code": "enrollment_token_exhausted",
"message": "This enrollment key is exhausted — it minted its max of 5 mailboxes. Issue a new key.",
"details": null,
"request_id": "req_7Yc2"
}
}
FieldMeaning
codeStable, machine-readable. Branch on this.
messageHuman-readable; safe to log, not to parse.
detailsField-level validation info on 422.
request_idAlso returned as X-Postern-Request-Id. Quote it in support requests.
StatusMeaningTypical error.code
400Malformed requestbad_request
401Missing / invalid / expired / revoked keyinvalid_api_key, invalid_enrollment_token, enrollment_token_revoked, enrollment_token_expired
402x402 payment required (test mode)payment_required
403Scope deniedscope_denied
404No such resource (or not yours)not_found
409Conflictenrollment_token_exhausted, idempotency_conflict
422Validation failedvalidation_failed (see details)
429Rate-limited / over a caprate_limited (see Retry-After)
5xxServer errorinternal_error

The TypeScript SDK throws a typed error for every non-2xx, extending ApiError:

import {
ApiError,
AuthenticationError, // 401
PermissionError, // 403
NotFoundError, // 404
ConflictError, // 409 — e.g. enrollment key exhausted
ValidationError, // 422 — see err.body.error.details
PaymentRequiredError, // 402 — x402 challenge in err.paymentRequired
RateLimitError, // 429 — err.retryAfter (seconds)
ConnectionError, // network failure before a response
TimeoutError, // request timed out / aborted
} from "@postern/sdk";
try {
await postern.inboxes.create();
} catch (err) {
if (err instanceof RateLimitError) {
await sleep((err.retryAfter ?? 1) * 1000);
} else if (err instanceof ApiError) {
console.error(err.status, err.code, err.message, err.requestId);
} else {
throw err;
}
}

Every ApiError carries status, code, requestId, body, and isClientError / isServerError.

GET and DELETE (idempotent) retry automatically on 429 / 5xx / network errors with jittered backoff that honors Retry-After. For POSTs, use client_id (create) or idempotency_key (send/reply) so a retry can’t duplicate.

A resource that belongs to another org returns 404, not 403 — Postern never confirms the existence of resources outside your tenant. A 403 means the resource is yours but your key lacks the required scope.