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