# Errors

Every error has a stable `code`, a plain message and a `retryable` flag. Retry only when it says yes, and wait for the `Retry-After` header when there is one.

- A failed screenshot is not charged.
- The number of failed screenshots per day is limited: 15 on free credits, 300 after a first payment.

**402, refused**

```json
{
  "error": {
    "code": "insufficient_credits",
    "message": "Not enough credits for this screenshot.",
    "retryable": false,
    "details": { "needed": 2, "available": 1 }
  }
}
```

## Refused before anything is captured

Nothing was tried, nothing was charged. The body is `{"error": {...}}`. With `invalid_request`, `details` names the parameters to fix.

| Code | HTTP | Retry | What it means |
| --- | --- | --- | --- |
| `invalid_request` | 422 | no | The request is not valid. Check the parameters listed in "details". |
| `unauthorized` | 401 | no | Missing or invalid API key. Send it in the "Authorization: Bearer" header. |
| `account_blocked` | 403 | no | Screenshots are paused for this account. Contact support. |
| `insufficient_credits` | 402 | no | Not enough credits for this screenshot. |
| `rate_limited` | 429 | yes | Too many requests. Wait a moment and try again. |
| `concurrency_limit` | 429 | yes | Too many screenshots running at once for this account. Wait for one to finish. |
| `failure_budget_exceeded` | 429 | no | Too many failed screenshots today for this account. Failed screenshots are free, so their number per day is limited. |
| `spend_limit_reached` | 503 | yes | Screenshots are temporarily paused. Try again later. |
| `url_not_allowed` | 422 | no | This URL cannot be captured. Only public http and https pages are allowed. |
| `domain_blocked` | 403 | no | This website cannot be captured with this service. |
| `domain_busy` | 429 | yes | This website is receiving too many screenshots right now. Try again in a minute. |
| `country_not_available` | 422 | no | This country is not available. See GET /v1/countries. |
| `service_paused` | 503 | yes | Screenshots are temporarily paused. Try again later. |
| `idempotency_conflict` | 409 | no | This Idempotency-Key was already used with different parameters. |
| `not_found` | 404 | no | Not found. |

**422, a parameter we do not know**

```json
{
  "error": {
    "code": "invalid_request",
    "message": "The request is not valid. Check the parameters listed in \"details\".",
    "retryable": false,
    "details": {
      "unknown_parameters": ["fullpage"],
      "allowed_parameters": ["url", "country", "viewport", "..."]
    }
  }
}
```

## The screenshot was tried and failed

The page was opened, or we tried to. Most of the time the body is the screenshot itself, with `status` set to `failed` and its `error` filled in, so you keep its id.

`option_unavailable` can also be answered before anything is tried, for example when you ask for a country that is not open to your account. The body is then `{"error": {...}}`, as in the first table.

| Code | HTTP | Retry | What it means |
| --- | --- | --- | --- |
| `country_not_obtained` | 502 | yes | The page could not be loaded from the requested country, so no screenshot was taken. You were not charged. Try again. |
| `challenge_page` | 502 | no | The website showed a bot check instead of the page. We do not bypass bot checks. |
| `timeout` | 504 | yes | The page took too long to load. |
| `page_too_heavy` | 502 | no | The page is too heavy to capture (over the size limit). |
| `too_many_redirects` | 502 | no | The page redirected too many times. |
| `selector_not_found` | 422 | no | No element matches "selector" on this page. |
| `wait_for_timeout` | 422 | no | The "wait_for" element never appeared. |
| `navigation_failed` | 502 | no | The page could not be opened. Check the address. |
| `image_too_large` | 502 | no | The screenshot would be too large. Use a smaller viewport or turn off full_page. |
| `option_unavailable` | 503 | no | One of the requested options is not available right now. |
| `image_withheld` | 502 | no | The page displays the network address used to load it, so the screenshot was not kept. You were not charged. |
| `engine_busy` | 503 | yes | All browsers are busy. Try again in a few seconds. |
| `canceled` | 409 | no | The screenshot was cancelled before it finished. |
| `internal_error` | 500 | yes | Something went wrong on our side. You were not charged. |

**502, tried and failed**

```json
{
  "id": "01JABCDEF2G3H4J5K6M7N8P9QR",
  "status": "failed",
  "image": null,
  "credits": { "charged": 0, "reserved": 1, "remaining": 49 },
  "error": {
    "code": "challenge_page",
    "message": "The website showed a bot check instead of the page. We do not bypass bot checks.",
    "retryable": false
  }
}
```

## What to do with an error

1. Read `error.code`, not the message: the code is stable, the wording may change.
2. If `retryable` is `false`, change the request before sending it again.
3. If `retryable` is `true`, wait for `Retry-After` when it is there, then try again with the same `Idempotency-Key`.
4. Stop after a few tries. See [Retries and Idempotency-Key](https://docs.localscreenshot.com/retries).

**429, wait and retry**

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 12
```

## Through the MCP server

Your AI gets the same message as a plain sentence, followed by the code, for example `(code: challenge_page, retryable: no)`. It can tell you what happened without reading JSON.
