> ## Documentation Index
> Fetch the complete documentation index at: https://docs.letmepost.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> The thirteen error codes, the response shape, and what to do about each one.

Every failure response from the API uses the same envelope:

```json error.json theme={"system"}
{
  "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"
  }
}
```

| field              | always set         | meaning                                      |
| ------------------ | ------------------ | -------------------------------------------- |
| `code`             | yes                | one of the thirteen 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         |

The shape never collapses to `{ body: {}, message: "" }`. If the upstream platform returned nothing meaningful, we still attach a `code`, `message`, and `requestId`.

## The thirteen codes

| code                                                   | typical HTTP                             | what it means                                                             |
| ------------------------------------------------------ | ---------------------------------------- | ------------------------------------------------------------------------- |
| [`validation_failed`](/errors/validation_failed)       | 400                                      | Request body or query failed schema validation.                           |
| [`preflight_failed`](/errors/preflight_failed)         | 400                                      | A documented platform constraint failed before the upstream call.         |
| [`platform_auth_failed`](/errors/platform_auth_failed) | 401, 403                                 | The connected account's token is missing, expired, or revoked.            |
| [`platform_rejected`](/errors/platform_rejected)       | 4xx (502 if mapped from an upstream 5xx) | Upstream rejected the call after preflight passed.                        |
| [`platform_unavailable`](/errors/platform_unavailable) | 502, 503                                 | Upstream is down or rate-limited; safe to retry later.                    |
| [`platform_not_enabled`](/errors/platform_not_enabled) | 403                                      | Platform is approval-gated upstream and not yet connectable for your org. |
| [`internal_error`](/errors/internal_error)             | 500                                      | Unexpected server-side issue. Includes a `requestId` to file.             |
| [`unauthenticated`](/errors/unauthenticated)           | 401                                      | Bearer header missing, malformed, or key revoked.                         |
| [`unauthorized`](/errors/unauthorized)                 | 403                                      | Authenticated, but not allowed to perform this action.                    |
| [`not_found`](/errors/not_found)                       | 404                                      | Resource doesn't exist or is out of scope.                                |
| [`idempotency_conflict`](/errors/idempotency_conflict) | 409                                      | Same `Idempotency-Key` reused with a different body.                      |
| [`rate_limited`](/errors/rate_limited)                 | 429                                      | Per-key rate limit exhausted. Honor `Retry-After`.                        |
| [`quota_exceeded`](/errors/quota_exceeded)             | 429                                      | Monthly plan post quota reached. Upgrade the plan or wait for the reset.  |

## Reading errors in code

The pattern is the same regardless of language:

```ts handle.ts theme={"system"}
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

<Steps>
  <Step title="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`.

    ```ts theme={"system"}
    // Stable
    if (err.code === "preflight_failed" && err.rule === "instagram.media.required") {
      attachDefaultImage();
    }

    // Brittle
    if (err.message.includes("Instagram requires media")) { /* … */ }
    ```
  </Step>

  <Step title="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.
  </Step>

  <Step title="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](https://status.letmepost.dev) if it persists.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

## See also

* Each code links to its own page above with reproduction, response shape, and remediation.
* [`preflight`](/preflight) for the full rule catalog when `code` is `preflight_failed`.
