# Errors

Every error is `application/problem+json` with a stable machine `code`. Branch on
`code`, never on `title` or on the HTTP status alone — several codes share a status
and mean entirely different things.

```jsonc
{
  "type": "https://apublished.com/docs/agents/errors#validation_failed",
  "title": "The post is not valid for one or more channels",
  "status": 422,
  "code": "validation_failed",
  "detail": "…",
  "violations": [ /* per-channel, with actual and allowed values */ ],
  "remediation": "…",          // what to do, machine-readable
  "context": { /* facts specific to this error */ }
}
```

### `unauthorized`

**HTTP 401** — Authentication required

Send `Authorization: Bearer ap_live_…`. Keys are created in the dashboard or via `POST /v1/keys`.

### `invalid_key`

**HTTP 401** — API key is invalid or revoked

The key does not exist or was revoked. Do not retry — get a new key.

### `key_expired`

**HTTP 401** — API key has expired

The key passed its `expiresAt`. Do not retry — mint a new one.

### `insufficient_scope`

**HTTP 403** — The key lacks the required scope

The key lacks a scope this call needs; `detail` names which. Note that `posts:write` without `posts:publish` can create scheduled posts and drafts but cannot publish immediately — that is the approval workflow, not a bug.

### `channel_not_permitted`

**HTTP 403** — The key is not scoped to this channel

The key is scoped to specific channels and this is not one of them. Use a broader key, or a channel it covers.

### `channel_reauth_required`

**HTTP 409** — The channel must be reconnected

The channel needs a human to reconnect it. Call `get_reauth_url` (MCP) or `POST /v1/channels/{id}:reauth` and give the URL to a person. This is routine — LinkedIn tokens expire every 60 days and cannot be refreshed programmatically.

### `channel_disabled`

**HTTP 409** — The channel is disabled

The channel was disconnected. Reconnect it before scheduling to it.

### `channel_paused`

**HTTP 409** — The channel is paused

The channel is paused. Posts to it are held rather than published. Resume it to continue.

### `channel.has_queue`

**HTTP 409** — The channel has scheduled posts

Disconnecting would cancel scheduled posts. `context` carries the count and the next due time. Re-send with `queue: "cancel_queued"` to accept that, or leave the channel connected and pause it instead.

### `validation_failed`

**HTTP 422** — The post is not valid for one or more channels

One or more channels rejected the post, and NOTHING was created. Each violation carries `actual`, `limit` and often `remediation`. Fix and resubmit — or call `:validate` first next time, which has no side effects.

### `settings.unknown_field`

**HTTP 422** — Unknown provider setting

A provider setting does not exist. Check `didYouMean` — it is usually a near-miss — and `schemaUrl` for the full settings schema.

### `content_rejected_by_platform`

**HTTP 422** — The platform rejected this content

The platform refused the content itself. This is terminal: retrying sends the same thing again.

### `media_rejected`

**HTTP 422** — The platform rejected this media

The platform refused the media. Check the size, duration and aspect-ratio limits for that provider.

### `post.locked`

**HTTP 409** — The post is already staged with the platform

The post is already staged with the platform — media uploaded, container created — so its content can no longer change. `context.allowed` lists what you can still do; normally that is cancel and recreate. Check `locksAt` on a target BEFORE attempting an edit rather than discovering this.

### `not_found`

**HTTP 404** — No such endpoint

Nothing is served at that URL. Check the path against `GET /v1/openapi.json`; this is not the same as a post or channel that does not exist, which have their own codes.

### `post_not_found`

**HTTP 404** — No such post

No such post in this tenant. Another tenant's id looks identical to a missing one, deliberately.

### `channel_not_found`

**HTTP 404** — No such channel

No such channel in this tenant.

### `request_too_large`

**HTTP 413** — The request body is too large

The body is larger than the API accepts. Media never travels through this endpoint — upload it with `POST /v1/media`, which presigns a direct upload, and reference the asset id instead.

### `rate_limited`

**HTTP 429** — Too many requests

Too many requests. Wait for `retryAfter` seconds. Do not retry sooner — it makes the wait longer.

### `quota_exhausted`

**HTTP 429** — The platform quota for today is exhausted

The platform quota for today is committed. Quotas are reserved at SCHEDULE time, so this means the day is genuinely full. Schedule for tomorrow, or check `GET /v1/channels/{id}/limits` first.

### `plan.limit_reached`

**HTTP 402** — The plan limit has been reached

The subscription limit was reached. `context` carries the current count and the ceiling.

### `idempotency_key_reused`

**HTTP 409** — This Idempotency-Key was used with a different body

The same Idempotency-Key was used with a DIFFERENT body. That is a bug in the caller: either reuse the key with the identical body to replay, or use a new key.

### `bad_request`

**HTTP 400** — The request could not be parsed

The request did not match the schema. `violations` names the fields.

### `internal`

**HTTP 500** — Something went wrong on our side

Something failed on our side. Retry with backoff; the Idempotency-Key makes that safe.

