# Errors and rate limits

> What each status code means, the error body, and how to back off.

## Error bodies

Every error returns an HTTP status and a JSON body with a readable `error`, plus `issues` when the request failed
validation:

```json
{
  "error": "You don't have permission to publish posts (post:publish).",
  "issues": []
}
```

Errors are written to be shown to a person or read by an agent: they say what was refused and why.

| Status | Meaning                                                                                                                       | What to do                                                                                  |
| ------ | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `400`  | The request is invalid: a field is missing, or the post breaks a network's rule (too long, wrong media). `issues` says which. | Fix the request. Don't retry it unchanged.                                                  |
| `401`  | No credential, a malformed one, or a revoked key or withdrawn sign-in.                                                        | Check the `Authorization` header.                                                           |
| `403`  | The credential is valid but lacks a permission (named in the message), or the plan doesn't include the feature.               | Use a credential with that permission, or upgrade.                                          |
| `404`  | It doesn't exist, or it belongs to another workspace.                                                                         | Check the id.                                                                               |
| `409`  | A conflict: the post already moved on (it published, or someone decided its approval first).                                  | Read the current state, then decide.                                                        |
| `429`  | Rate limited.                                                                                                                 | Wait for `Retry-After` seconds, then retry.                                                 |
| `502`  | A network or provider we called failed, or our access to it is broken.                                                        | Retry later; if it persists, reconnect the channel.                                         |
| `500`  | Something went wrong on our side.                                                                                             | Retry later, and tell [support](https://orbitwrite.com/docs/guides/support) if it persists. |

## Rate limits

Limits are counted per credential (each API key, or each signed-in person), never per workspace, so one busy
integration can't lock your team out.

- Endpoints that cost something, such as AI generation, link previews, GIF search, inbox replies and forced inbox
  syncs, each have their own budget.
- The MCP server allows 240 tool calls a minute per credential.

A `429` carries a `Retry-After` header in seconds. A `429` is always safe to retry, because the limiter refuses before
anything runs.

## Retrying writes safely

Don't blindly retry a `POST` that failed with a `5xx` or a dropped connection: a `POST /posts` may already have
committed, and replaying it publishes twice. Read the post back (`GET /posts`, or `GET /posts/publish-status`)
first. The [SDK](https://orbitwrite.com/docs/developers/sdk#errors-and-retries) follows this rule for you.

## Platform limits

Network limits, like X's 280 characters or Instagram's five hashtags, are enforced on our side before anything is
sent, and come back as a `400` with the reason. Fit posts up front with [`GET /api/v1/platforms`](https://orbitwrite.com/docs/platforms), which
serves the same rules.
