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

# Errors and status codes

> Every HTTP status code Simkl returns, what it means, and how to handle it.

Simkl uses standard HTTP status codes. Error responses always have a JSON body with `error` (machine-readable identifier), `code` (integer — the HTTP status code echoed in the body), and an optional `message` (human-readable guidance).

```json title="Standard error envelope" theme={"theme":{"light":"github-light","dark":"vesper"}}
{
  "error":   "client_id_failed",
  "code":    412,
  "message": "Your client_id is wrong. Try another one"
}
```

<Warning>
  **Branch on `error`, never on `message`.** The `error` identifier is the contract — it's stable across releases. The `message` text is developer-facing prose that may be reworded at any time. Parsing `message` will break your client.
</Warning>

## At a glance

| Code                            | Name                  | Retry?                         |
| ------------------------------- | --------------------- | ------------------------------ |
| [`200`](#ok)                    | OK                    | —                              |
| [`201`](#created)               | Created               | —                              |
| [`204`](#no-content)            | No Content            | —                              |
| [`302`](#found)                 | Found                 | —                              |
| [`400`](#bad-request)           | Bad Request           | No — fix the request           |
| [`401`](#unauthorized)          | Unauthorized          | No — re-authenticate           |
| [`403`](#forbidden)             | Forbidden             | No — fix the app or contact us |
| [`404`](#not-found)             | Not Found             | No                             |
| [`409`](#conflict)              | Conflict              | No                             |
| [`412`](#client-id-failed)      | client\_id failed     | No                             |
| [`429`](#too-many-requests)     | Too Many Requests     | Yes — back off                 |
| [`500`](#internal-server-error) | Internal Server Error | Yes — exponential backoff      |
| [`502`](#bad-gateway)           | Bad Gateway           | Yes — exponential backoff      |
| [`503`](#service-unavailable)   | Service Unavailable   | Yes — exponential backoff      |

<Note>
  **Not every endpoint can return every code.** Each endpoint's reference page lists the codes that actually fire there — many endpoints can only return a subset (e.g., `/sync/activities` can only return `200`, `401`, `412`, `429`, `500`). The 4xx/5xx pages below describe the codes; check the per-endpoint reference for which apply.
</Note>

## Success codes

### OK

`200` — Success. The body contains the resource you requested.

### Created

`201` — Success. A new resource was created. Typical for `POST /sync/...` and `POST /scrobble/...`.

### No Content

`204` — Success but the response body is empty. Typical for `DELETE` endpoints.

### Found

`302` — A redirect. Follow the `Location` header. Most commonly returned by [the redirect endpoint](/api-reference/simkl/redirect).

## 4xx — your request

### Bad Request

`400` — The request was malformed. The `message` field usually identifies the offending parameter.

**Real `error` values returned**

| `error`           | When                            | Example `message`       |
| ----------------- | ------------------------------- | ----------------------- |
| `empty_field`     | A required field is missing     | `Missed "to" parameter` |
| `wrong_parameter` | A field has an unaccepted value | `Wrong "to" parameter`  |

**Fix**
Read `message`, correct the request, then resend. Don't retry without changing the request.

```json title="400 Bad Request" theme={"theme":{"light":"github-light","dark":"vesper"}}
{
  "error":   "empty_field",
  "code":    400,
  "message": "Missed \"to\" parameter"
}
```

### Unauthorized

`401` — Missing or invalid user access token.

**Common causes**

* `Authorization` header missing on a token-required endpoint.
* The user's `access_token` was revoked at [Connected Apps settings](https://simkl.com/settings/connected-apps/).
* A typo or truncation in the token value.

**Fix**
Re-authenticate with [OAuth 2.0 or PIN](/authentication). Simkl tokens advertise `expires_in: 157680000` (5 years) on mint, so a 401 in practice means the user removed your app from their account or the token never matched in the first place.

```json title="401 Unauthorized" theme={"theme":{"light":"github-light","dark":"vesper"}}
{
  "error": "user_token_failed",
  "code":  401
}
```

The 401 response also carries an RFC 6750 §3 `WWW-Authenticate` header for clients that key off it:

```http theme={"theme":{"light":"github-light","dark":"vesper"}}
HTTP/1.1 401 Unauthorized
Content-Type: application/json
WWW-Authenticate: Bearer realm="api.simkl.com", error="user_token_failed"
```

### Forbidden

`403` — Refused. The request was understood but the caller is not permitted to perform it.

**Known `error` values**

| `error`           | When                                                                                        |
| ----------------- | ------------------------------------------------------------------------------------------- |
| `redirect_failed` | OAuth/PIN `redirect` parameter doesn't match a URL registered for your app                  |
| `forbidden`       | Generic refusal — feature gated to Simkl Pro/VIP, or an action the user hasn't consented to |

**Fix**
Match the redirect URL exactly to what's registered in your [developer settings](https://simkl.com/settings/developer/). For Pro/VIP-gated features, surface an upgrade prompt rather than retrying.

```json title="403 Forbidden — redirect mismatch" theme={"theme":{"light":"github-light","dark":"vesper"}}
{
  "error":   "redirect_failed",
  "code":    403,
  "message": "Invalid redirect_uri. This value must match a URL registered with the API Key."
}
```

### Not Found

`404` — The URL or resource doesn't exist.

<Tip>
  **Most catalog endpoints don't return 404 for missing IDs.** Hitting `/movies/99999999` returns `200 []` (empty array), not 404. Use `404` documentation in per-endpoint references as the source of truth — most read-only catalog endpoints simply return empty.
</Tip>

```json title="404 Not Found" theme={"theme":{"light":"github-light","dark":"vesper"}}
{
  "error": "id_err",
  "code":  404
}
```

### Conflict

`409` — The resource state conflicts with the request.

**Common causes**

* Tried to `POST /scrobble/stop` on a session that completed in the last hour.
* Tried to perform a write that would duplicate an existing record.

**Fix**
Treat `409` as soft-success when scrobbling — the user already finished the episode. Don't retry the same call. The body includes `watched_at` (when the original watch landed) and `expires_at` (when the 1-hour duplicate-window closes), so you can show the user the existing watch state instead of re-firing the call.

```json title="409 Conflict (POST /scrobble/stop)" theme={"theme":{"light":"github-light","dark":"vesper"}}
{
  "error":      "already_watched",
  "watched_at": "2026-05-08T14:00:00Z",
  "expires_at": "2026-05-08T15:00:00Z"
}
```

### client\_id failed

`412` — Your `client_id` is missing, wrong, suspended, or has hit a request limit.

**Common causes**

* Typo in `client_id` (use the value from [developer settings](https://simkl.com/settings/developer/), not a screenshot).
* App was suspended for a Terms of Service violation.
* App hit a per-`client_id` request cap — see [Rate limits](/resources/rate-limits).

**Fix**
Double-check the value. If it's correct, [reach out on Discord](https://discord.gg/MJsWNE4).

```json title="412 client_id failed" theme={"theme":{"light":"github-light","dark":"vesper"}}
{
  "error":   "client_id_failed",
  "code":    412,
  "message": "Your client_id is wrong. Try another one"
}
```

### Too Many Requests

`429` — Rate limit hit.

**Fix**
Implement [exponential backoff](#exponential-backoff). Cache responses aggressively. Use [Trending](/api-reference/trending) and [Calendar](/api-reference/calendar) CDN files instead of polling. Full guidance: [Rate limits](/resources/rate-limits).

```json title="429 Too Many Requests" theme={"theme":{"light":"github-light","dark":"vesper"}}
{
  "error": "rate_limit",
  "code":  429
}
```

## 5xx — our problem

### Internal Server Error

`500` — Something broke on our side.

**Fix**
Retry with exponential backoff. If the error persists for more than \~30 seconds, [report it on Discord](https://discord.gg/MJsWNE4).

```json title="500 Internal Server Error" theme={"theme":{"light":"github-light","dark":"vesper"}}
{
  "error": "internal",
  "code":  500
}
```

### Bad Gateway

`502` — Simkl is being upgraded or briefly unreachable.

**Fix**
Retry with exponential backoff. Check [status](https://status.simkl.com/) for known issues.

### Service Unavailable

`503` — Servers are up but overloaded.

**Fix**
Retry after the delay. Respect any `Retry-After` header.

## Handling errors gracefully

### Exponential backoff

Recommended retry pattern for transient errors (`429`, `500`, `502`, `503`):

| Attempt | Wait before retry         |
| ------- | ------------------------- |
| 1       | 1 s                       |
| 2       | 2 s                       |
| 3       | 4 s                       |
| 4       | 8 s                       |
| 5       | 16 s (give up after this) |

Add jitter (random 0–1 s) so multiple clients don't synchronize.

```js title="retry-with-backoff.js" theme={"theme":{"light":"github-light","dark":"vesper"}}
async function retry(fn, attempts = 5) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (err) {
      if (i === attempts - 1 || ![429, 500, 502, 503].includes(err.status)) throw err;
      const delay = Math.min(1000 * 2 ** i, 60000) + Math.random() * 1000;
      await new Promise((r) => setTimeout(r, delay));
    }
  }
}
```

### Don't retry deterministic errors

`400`, `401`, `403`, `404`, `409`, `412` mean something is wrong with the request itself. Retrying without changing the request just wastes quota and triggers `429`.

### User-facing messaging

`message` is human-readable but written for developers. For end users:

| Code  | Suggested user message                                 |
| ----- | ------------------------------------------------------ |
| `401` | *Please sign in again to continue.*                    |
| `403` | *This action isn't available for your account.*        |
| `404` | *We couldn't find that title.*                         |
| `429` | *Slow down — try again in a moment.*                   |
| `5xx` | *Simkl is having a moment. We'll retry automatically.* |

### Logging

Always log `error` and `code` together with your request URL. `error` is the stable contract you'll branch on; `code` confirms the HTTP class.
