Skip to main content
Every failure response from the API uses the same envelope:
error.json
{
  "error": {
    "code": "preflight_failed",
    "message": "Post text is 312 graphemes; Bluesky allows at most 300.",
    "rule": "bluesky.text.max_graphemes",
    "platform": "bluesky",
    "platformVersion": "atproto-2026-04",
    "platformResponse": null,
    "remediation": "Shorten the post to 300 graphemes or fewer.",
    "docUrl": "https://docs.letmepost.dev/errors/preflight_failed",
    "ruleUrl": "https://docs.letmepost.dev/preflight/bluesky-text-max_graphemes",
    "requestId": "req_01HY6X4AWBJM2K9F2PTQMRD9JQ"
  }
}
fieldalways setmeaning
codeyesone of the twelve values below
messageyeshuman-readable explanation
rulewhen knownpreflight rule id or validation field path
platformwhen knownwhich upstream platform was involved
platformVersionwhen knownthe pinned upstream API version we targeted
platformResponsewhen knownraw upstream body, untouched
remediationusuallyactionable next step
docUrlalwaysabsolute link to this code’s docs page
ruleUrlwhen rule is setabsolute link to the rule’s preflight page
requestIdalwaysechoed in the x-request-id response header
traceIdwhen onOTel trace id when tracing is active
The shape never collapses to { body: {}, message: "" }. If the upstream platform returned nothing meaningful, we still attach a code, message, and requestId.

The twelve codes

codetypical HTTPwhat it means
validation_failed400Request body or query failed schema validation.
preflight_failed400A documented platform constraint failed before the upstream call.
platform_auth_failed401, 403The connected account’s token is missing, expired, or revoked.
platform_rejected4xx (502 if mapped from an upstream 5xx)Upstream rejected the call after preflight passed.
platform_unavailable502, 503Upstream is down or rate-limited; safe to retry later.
internal_error500Unexpected server-side issue. Includes a requestId to file.
unauthenticated401Bearer header missing, malformed, or key revoked.
unauthorized403Authenticated, but not allowed to perform this action.
not_found404Resource doesn’t exist or is out of scope.
idempotency_conflict409Same Idempotency-Key reused with a different body.
rate_limited429Per-key rate limit exhausted. Honor Retry-After.

Reading errors in code

The pattern is the same regardless of language:
handle.ts
const res = await fetch(url, init);
if (!res.ok) {
  const body = await res.json();
  // body.error is the envelope above.
  if (body.error.code === "preflight_failed") {
    // body.error.rule is the canonical rule id.
    // Look it up at /docs/preflight/<rule>/ for context.
    throw new PreflightError(body.error.rule, body.error.message);
  }
  if (body.error.code === "rate_limited") {
    const retry = Number(res.headers.get("retry-after") ?? 5);
    await sleep(retry * 1000);
    return retryOnce();
  }
  throw new Error(\`\${body.error.code}: \${body.error.message}\`);
}

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

1

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.
// Stable
if (err.code === "preflight_failed" && err.rule === "instagram.media.required") {
  attachDefaultImage();
}

// Brittle
if (err.message.includes("Instagram requires media")) { /* … */ }
2

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.
3

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.
4

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.
5

Always grep by `requestId`

Every response carries a requestId in the body and the x-request-id response header. When you open a support issue or check the logs, include it — it indexes the full request log for that operation across the entire pipeline.

See also

  • Each code links to its own page above with reproduction, response shape, and remediation.
  • preflight for the full rule catalog when code is preflight_failed.