Menu

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:

{
  "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.
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.
signature_is_required 403 The workspace requires 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.

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.

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.

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));
  }
}