---
title: Errors
description: The error response shape and what each HTTP status code means for your client.
section: Core concepts
order: 3
---

# Errors

Every error the v3 API returns uses the same JSON envelope, so your client can handle failures with one code path instead of one per endpoint.

```json
{
  "error": "not_found",
  "detail": "movie with tmdb_id 999999999 not found"
}
```

`error` is always present and is a machine-readable code you can switch on. `detail` adds a human-readable explanation when FlickList has one to give. Some error codes, like a plain `unauthorized`, omit `detail` entirely rather than sending it as `null`.

Every surface uses this same envelope, whether the failure came from Sync, Scrobble, Lists, or the catalog endpoints. A scope failure looks the same shape as a validation failure:

```json
{
  "error": "forbidden",
  "detail": "..."
}
```

| Status | Meaning | What to do |
| --- | --- | --- |
| 400 | The request failed validation. | Fix the request body or params per `detail`. Retrying unmodified fails again. |
| 401 | The credential is missing, invalid, expired, or revoked. | Sign in again through the device-code flow, or check the API key you're sending. |
| 403 | The credential is valid but lacks the scope this endpoint needs. | Use a credential with the `read` or `write` scope required. See [Authentication](/dev/authentication). |
| 404 | Nothing matches the identifier you sent. | Double check the ID. The item may not exist in FlickList's catalog at all. |
| 429 | You've exceeded the per-credential rate limit. | Read the `Retry-After` header (seconds) and wait before retrying. See [Rate limits & fair use](/dev/rate-limits). |

Adding something that already exists (an item already on your watchlist, a rating you already set) is not an error: those writes are idempotent and succeed, with the response telling you what was already there. See the individual [Your data](/dev/sync/watchlist) pages for each endpoint's exact response.

Branch on the HTTP status code first, `error` second. The status code tells you the category; the `error` string tells you which specific case it was, when you need that level of detail:

```javascript
const res = await fetch(url, { headers });
if (!res.ok) {
  const body = await res.json();
  if (res.status === 429) {
    // wait Number(res.headers.get('retry-after')) seconds, then retry
  } else {
    console.error(body.error, body.detail);
  }
}
```

> [!TIP]
> v3 is additive-only. New fields can appear in any response, including error responses, at any time. Don't write parsing code that fails on a field it doesn't recognize.
