Screenshots

Capture a screenshot of any URL — a working request, the options that matter, the response, what it costs, and the errors to handle.

For AI agents: https://bytekit.com/llms.txt — every docs page is available as markdown by appending .md

POST /v1/screenshots loads a URL in a real browser and returns an image of the page. Send a URL and you get back a jpeg (the default) or png, plus the page title and the image dimensions. Everything else — device, viewport, how long to wait, what to hide — is optional.

Capture a screenshot

curl -X POST https://api.bytekit.com/v1/screenshots \
  -H "Authorization: Bearer sk_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}'

By default the call is synchronous: the request is held open for up to 28 seconds and returns 200 with the finished screenshot. The response is JSON — image_url is a link to the image, not the image bytes:

{
  "id": "ss_01a1210c210477f6838a6256629087e6",
  "status": "completed",
  "url": "https://example.com",
  "image_url": "https://<storage-host>/.../screenshot.jpeg?X-Amz-Signature=...",
  "page_title": "Example Domain",
  "image_width": 1440,
  "image_height": 900,
  "file_size_bytes": 62635,
  "credits_charged": 1,
  "billing": {
    "raw_bytes": 334,
    "factors": [
      {
        "name": "phase1_render",
        "value": 1.5,
        "reason": "phase 1 proxy fetch + render overhead"
      }
    ],
    "multiplier": 1.5,
    "billed_bytes": 501
  },
  "error_code": null,
  "error_message": null,
  "created_at": "2026-10-09T14:23:25.417Z",
  "completed_at": "2026-10-09T14:23:33.047Z",
  "expires_at": "2027-10-09T14:23:25.407Z",
  "poll_url": "https://api.bytekit.com/v1/screenshots/ss_01a1210c210477f6838a6256629087e6"
}

This response was returned by a real request. Two values are edited for the page: the signed image_url is shortened, and poll_url shows the production host.

FieldDescription
idScreenshot id, prefixed ss_. Use it with GET /v1/screenshots/{id}
statuspending, processing, completed, or failed
image_urlSigned link to the image. It stays valid for 10 minutes — fetch a fresh one by polling GET /v1/screenshots/{id}
page_titleThe page's <title>
image_width, image_heightImage dimensions in pixels
file_size_bytesSize of the image file. Informational only — it is not what you are billed on (see Cost)
billingWhat the capture was billed: raw_bytes, the factors applied, the resulting multiplier, and billed_bytes
error_code, error_messageWhy a capture failed. null on success
expires_atWhen the stored image expires
poll_urlWhere to poll this screenshot

The 200 response also carries X-Screenshot-* headers (the same id, status and timestamps as the JSON) and X-Quota-* headers reporting your quota.

If the capture is still running when the 28 seconds are up, the call returns 202 with status: "processing" and the same id. The capture keeps running in the background — poll it as described in Async captures.

Options

Send any of these alongside url. All are optional and the defaults suit most pages. Unknown fields are rejected with 422 validation_error.

curl -X POST https://api.bytekit.com/v1/screenshots \
  -H "Authorization: Bearer sk_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "format": "png",
    "viewport": {"width": 1280, "height": 720},
    "dark_mode": true,
    "wait_until": "networkidle"
  }'

This request was run for real and returned 200 with "image_width": 1280 and "image_height": 720.

Output

OptionDefaultDescription
formatjpegImage format: jpeg or png
quality80Image quality, an integer from 1 to 100
full_pagefalseCapture the whole scrollable page instead of only the viewport
clipnullCapture only a region: {"x": 0, "y": 0, "width": 800, "height": 600} in pixels (width and height at least 1)
device_scale_factornullPixel density: 1 or 2 (2 for a retina-sharp image)

Device

OptionDefaultDescription
devicedesktopdesktop or mobile
viewportnullBrowser window size, {"width": 1280, "height": 720} in pixels. Overrides the device default
dark_modefalseRender the page with a dark colour scheme preference

Page readiness

OptionDefaultDescription
wait_untilloadNavigation event to wait for: load, domcontentloaded, or networkidle
wait_for_selectornullCSS selector that must be visible before the capture is taken
delay_ms0Extra wait after the page is ready, in milliseconds (0 to 10000)
scrolltrueScroll the page to the bottom before capturing, so lazy-loaded content appears

wait_until and wait_for_selector are Screenshot options. They are not POST /v1/scrape options — sending either there returns 422. See the Scraping guide.

Clean-up

OptionDefaultDescription
block_adstrueBlock ads on the page
block_cookie_bannerstrueHide cookie-consent banners

Geo and language

OptionDefaultDescription
countryUSTwo-letter, upper-case country code the page is requested from
languagenullBrowser language, for example de-DE. When null, the language follows country

Other

OptionDefaultDescription
include_htmlfalseAlso capture the page's rendered HTML alongside the image. The HTML is not returned in the response
metadata{}Your own key-value pairs to attach to the capture. At most 4096 bytes as JSON; keys must not start with _

cookies and headers are not request options on any capture endpoint. Sending either returns 400 validation_error.

Async captures

For pages that take a while, add ?async=true to the URL, or send "async": true in the request body. Either one is enough, and false in one never cancels true in the other. The call returns 202 immediately with an ss_ id and status: "pending", and the capture runs in the background. POST /v1/scrape accepts the same two spellings.

# Start an async capture
curl -X POST "https://api.bytekit.com/v1/screenshots?async=true" \
  -H "Authorization: Bearer sk_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com"}'

# Poll for the result
curl https://api.bytekit.com/v1/screenshots/ss_... \
  -H "Authorization: Bearer sk_live_YOUR_KEY_HERE"

The first call returns:

{
  "id": "ss_01a1210d0e3a719589e9c3d70c4791d2",
  "status": "pending",
  "url": "https://example.com",
  "image_url": null,
  "page_title": null,
  "image_width": null,
  "image_height": null,
  "file_size_bytes": null,
  "credits_charged": 1,
  "error_code": null,
  "error_message": null,
  "created_at": "2026-10-09T14:24:26.172Z",
  "completed_at": null,
  "expires_at": "2027-10-09T14:24:26.149Z",
  "poll_url": "https://api.bytekit.com/v1/screenshots/ss_01a1210d0e3a719589e9c3d70c4791d2"
}

This response was returned by a real request; poll_url shows the production host.

GET /v1/screenshots/{id} answers by state:

StateStatusBody
Still pending or processing202The screenshot resource; poll again
completed200The full screenshot resource, with a fresh signed image_url
failed200The error envelope, with the failure in error.code

A failed capture is a successfully retrieved terminal state, so check status before reading image_url.

Cost

Screenshots are billed on bandwidth, not credits. A capture is billed on the bytes of the initial page fetch, multiplied by 1.5 — the extra 0.5 covers rendering the page. The size of the image file is not the basis. If that initial fetch is skipped or fails, the capture bills nothing.

In the example above, the page fetch transferred 334 bytes (raw_bytes), so the capture was billed 501 bytes (billed_bytes). The billing object is the authoritative breakdown; its factors[].reason text is descriptive and may change, so read value and multiplier rather than parsing it. The same figures arrive in the X-Raw-Bytes, X-Billing-Multiplier, and X-Billable-Bytes response headers.

The credits_charged field and the X-Screenshot-Credits-Charged header carry a legacy weight (1, 2 or 4, doubling for full_page and again for retina or mobile). It is recorded for reporting only and is not what you are billed. For how capture bandwidth is billed, see Bandwidth billing.

Errors

Errors use the unified envelope described in the Errors guide. These are the ones you are most likely to meet on this endpoint:

StatusCodeWhenRetry?
400invalid_urlurl is not a valid HTTP(S) URL. error.details.reason says why, for example scheme_unsupportedNo — fix the URL
400request_too_largeThe request body is over 512 KBNo — send a smaller body
401unauthorized, invalid_api_keyThe API key is missing, malformed, or not validNo — check your key
400validation_errorThe request carries a removed field such as cookies or headersNo — remove the field
402quota_exceededThe balance cannot fund the capture. error.details.required_bytes and available_bytes give the numbersNo — add balance or upgrade
402spending_cap_reached, billing_entitlement_impaired, billing_entitlement_expiredThe spending cap or the subscription state blocks the request — see the Errors guideNo — resolve the billing state first
403host_forbidden, path_forbiddenThe URL's host or path is not allowed as a capture targetNo — use a different URL
422validation_errorA field failed validation (error.details names the field), an unknown field was sent, or the body is not valid JSON (no details)No — fix the request
422blocked, timeout, …The capture ran and failed inside the 28-second hold. The body is the screenshot resource, with error_code and error_message setPossibly — a retry is a new capture
429rate_limitedToo many requests per secondYes — wait for the Retry-After header, then back off
429concurrency_limitToo many captures in flight at onceYes — wait for the Retry-After header
404not_foundGET /v1/screenshots/{id} for an id that does not exist on your accountNo

A 422 has two shapes. A validation failure carries the error envelope, where error is an object. A failed capture carries the screenshot resource instead — no error object, but flat error_code and error_message fields and an id you can look up later. Tell them apart by whether the top-level error key exists, not by status: both carry "status": "failed".

A validation failure looks like this (returned by a real request with "quality": 150):

{
  "status": "failed",
  "error": {
    "code": "validation_error",
    "message": "Invalid request body.",
    "http_status": 422,
    "failed_at": "2026-10-09T14:24:39.662Z",
    "details": {
      "formErrors": [],
      "fieldErrors": { "quality": ["Number must be less than or equal to 100"] }
    }
  }
}

Next steps