error.json
| field | always set | meaning |
|---|---|---|
code | yes | one of the twelve values below |
message | yes | human-readable explanation |
rule | when known | preflight rule id or validation field path |
platform | when known | which upstream platform was involved |
platformVersion | when known | the pinned upstream API version we targeted |
platformResponse | when known | raw upstream body, untouched |
remediation | usually | actionable next step |
docUrl | always | absolute link to this code’s docs page |
ruleUrl | when rule is set | absolute link to the rule’s preflight page |
requestId | always | echoed in the x-request-id response header |
traceId | when on | OTel trace id when tracing is active |
{ body: {}, message: "" }. If the upstream platform returned nothing meaningful, we still attach a code, message, and requestId.
The twelve codes
| code | typical HTTP | what it means |
|---|---|---|
validation_failed | 400 | Request body or query failed schema validation. |
preflight_failed | 400 | A documented platform constraint failed before the upstream call. |
platform_auth_failed | 401, 403 | The connected account’s token is missing, expired, or revoked. |
platform_rejected | 4xx (502 if mapped from an upstream 5xx) | Upstream rejected the call after preflight passed. |
platform_unavailable | 502, 503 | Upstream is down or rate-limited; safe to retry later. |
internal_error | 500 | Unexpected server-side issue. Includes a requestId to file. |
unauthenticated | 401 | Bearer header missing, malformed, or key revoked. |
unauthorized | 403 | Authenticated, but not allowed to perform this action. |
not_found | 404 | Resource doesn’t exist or is out of scope. |
idempotency_conflict | 409 | Same Idempotency-Key reused with a different body. |
rate_limited | 429 | Per-key rate limit exhausted. Honor Retry-After. |
Reading errors in code
The pattern is the same regardless of language:handle.ts
Why this matters
Every code on this page is narrow, stable, and documented. If we discover a new failure shape, we add a new code with its own page rather than fold it into an existing one — broad error codes that mask multiple underlying causes are the failure mode this design exists to defeat.Best practices
Branch on `code`, never on `message`
Error messages are tuned for humans and can change between releases. The
code field is part of the API contract — it’s stable. Always switch on code (and rule for preflight failures), never on substring match against message.Respect `Retry-After` and the `X-RateLimit-Limit` ceiling
On
rate_limited responses, the Retry-After header tells you how many seconds to wait. The X-RateLimit-Limit header is on every response (not just 429s) and carries the static per-route ceiling — useful to size your client-side concurrency without first probing a 429. We deliberately do not publish RateLimit-Remaining or RateLimit-Reset in v1: until per-key counting lands, those values would be misleading across tenants. Honor Retry-After on 429 and you’ll never need them.Treat platform 4xx as caller-fixable, 5xx as platform-fault
Platform
4xx responses (mapped to letmepost platform_rejected with the upstream platformResponse) usually mean the post itself violated platform rules — character count, missing media, banned URL pattern. These can be fixed by editing the post.Platform 5xx responses (mapped to platform_unavailable) are upstream issues. Retry with backoff; check the status page if it persists.Follow `docUrl` for unknown codes
Every error response includes a
docUrl that points at the corresponding code’s docs page, and a ruleUrl when rule is present. If you hit an error code you don’t recognize, the docUrl value will take you to its remediation page directly.See also
- Each code links to its own page above with reproduction, response shape, and remediation.
preflightfor the full rule catalog whencodeispreflight_failed.

