API · Errors
Every non-2xx response uses the same envelope, with a stable error.code you can branch on
without parsing prose.
The envelope
Section titled “The envelope”{ "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" }}| Field | Meaning |
|---|---|
code | Stable, machine-readable. Branch on this. |
message | Human-readable; safe to log, not to parse. |
details | Field-level validation info on 422. |
request_id | Also returned as X-Postern-Request-Id. Quote it in support requests. |
Status codes
Section titled “Status codes”| Status | Meaning | Typical error.code |
|---|---|---|
400 | Malformed request | bad_request |
401 | Missing / invalid / expired / revoked key | invalid_api_key, invalid_enrollment_token, enrollment_token_revoked, enrollment_token_expired |
402 | x402 payment required (test mode) | payment_required |
403 | Scope denied | scope_denied |
404 | No such resource (or not yours) | not_found |
409 | Conflict | enrollment_token_exhausted, idempotency_conflict |
422 | Validation failed | validation_failed (see details) |
429 | Rate-limited / over a cap | rate_limited (see Retry-After) |
5xx | Server error | internal_error |
SDK error classes
Section titled “SDK error classes”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.
Retries
Section titled “Retries”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.
404 vs 403 — tenancy
Section titled “404 vs 403 — tenancy”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.
- Rate limits & quotas — what triggers
429, and the headers. - x402 test mode — the
402flow. - Enrollment — the
401/409enrollment cases.