# Errors

> Every error code the API returns, what causes it, and what to do about it.

## Error format

Errors are JSON, with an HTTP status of 400 or above:

```json
{
  "is_successful": false,
  "error_code": "too_many_requests",
  "error_message": "Rate limit of 10 requests per minute exceeded.",
  "retry_after": 12,
  "limit": 10
}
```

Branch on `error_code`, which is stable; `error_message` is for people and may change. Some errors add fields, listed below. When a request can be retried later, the response also has a `Retry-After` header in seconds.

Failed requests don't count toward your monthly screenshots.

## Request errors

| Code | Status | Cause |
| --- | --- | --- |
| `invalid_request` | 400 | The POST body isn't a JSON object. |
| `invalid_parameter` | 400 | An unknown option, a value of the wrong type or out of range, or options that don't go together. The message says which. |
| `invalid_url` | 400 | `url` isn't a valid `http(s)` URL. |
| `access_key_invalid` | 401 | No access key, or not a valid one. See [Authentication](https://shotkit.net/docs/authentication.md). |
| `email_not_verified` | 403 | On the Hobby plan, nobody in the workspace has verified their email address yet. |
| `signature_is_not_valid` | 403 | The signature doesn't match the query string (GET) or the body (POST). See [signed requests](https://shotkit.net/docs/authentication.md#signed-requests). |
| `signature_is_required` | 403 | The workspace [requires signed requests](https://shotkit.net/docs/authentication.md#require-signed-requests) and this one has no signature. |

## Limits

| Code | Status | Extra fields | Cause |
| --- | --- | --- | --- |
| `too_many_requests` | 429 | `retry_after`, `limit`, `upgrade_url` | Over your plan's requests per minute. Wait `retry_after` seconds. |
| `screenshots_limit_reached` | 429 | `limit`, `resets_at`, `retry_after`, `upgrade_url` | This month's screenshots are used up. Resets on the 1st (UTC). |

`upgrade_url` is left out on the largest plan. See [Limits](https://shotkit.net/docs/limits.md).

## Page errors

| Code | Status | Extra fields | Cause |
| --- | --- | --- | --- |
| `host_returned_error` | 400 | `returned_status_code` | The page returned 4xx or 5xx. Set `ignore_host_errors=true` to capture it anyway. |
| `name_not_resolved` | 400 | | The domain doesn't exist. |
| `network_error` | 400 | | The page couldn't be loaded (connection refused, TLS error, …). The message has Chrome's error. |
| `timeout_error` | 504 | | The page or the whole request took longer than `navigation_timeout` or `timeout`. |
| `selector_not_found` | 400 | | `wait_for_selector`, `click` or `hover` didn't match a visible element, or `selector`/`scroll_into_view` didn't with `error_on_selector_not_found=true`. |
| `script_triggers_error` | 400 | | Your `scripts` threw. The message has the error. |

## Checks

These come from the `fail_if_*` options, which turn a bad page into an error instead of a screenshot. See [Clean screenshots](https://shotkit.net/docs/clean-screenshots.md#fail-instead-of-capturing-a-broken-page).

| Code | Status | Extra fields | Cause |
| --- | --- | --- | --- |
| `content_missing_specified_string` | 400 | `missing_string` | A `fail_if_content_missing` text isn't on the page. |
| `content_contains_specified_string` | 400 | `matched_string` | A `fail_if_content_contains` text is on the page. |
| `matched_failed_request` | 400 | `failed_request_url` | A request matching `fail_if_request_failed` failed or returned 4xx/5xx. |
| `vision_error` | 400 | | OpenAI rejected the vision request. The message has OpenAI's error. |

## Server errors

| Code | Status | Extra fields | Cause |
| --- | --- | --- | --- |
| `server_busy` | 503 | `retry_after` | All browsers are busy. Retry after a few seconds. |
| `queue_unavailable` | 503 | `retry_after` | An `async` request couldn't be queued. Retry shortly. |
| `video_not_supported` | 501 | | Video output isn't available on this server. |
| `internal_application_error` | 500 | | Something went wrong on our side. Retry; if it keeps happening, contact us. |

## Retrying

Retry `429`, `503` and `504` responses, waiting at least `Retry-After` seconds. Don't retry other `4xx` errors without changing the request; they fail the same way every time.

```js
async function take(params, attempts = 3) {
  for (let i = 1; ; i++) {
    const res = await fetch("https://shotkit.net/api/take", {
      method: "POST",
      headers: { "X-Access-Key": process.env.SHOTKIT_ACCESS_KEY, "Content-Type": "application/json" },
      body: JSON.stringify(params),
    });
    if (res.ok) return Buffer.from(await res.arrayBuffer());
    const err = await res.json();
    const retryable = [429, 503, 504].includes(res.status) && err.error_code !== "screenshots_limit_reached";
    if (!retryable || i >= attempts) throw new Error(`${err.error_code}: ${err.error_message}`);
    await new Promise((r) => setTimeout(r, Number(res.headers.get("retry-after") ?? 5) * 1000));
  }
}
```
