Instant Snap API
Create a capture
One endpoint does the work of the whole Instant Snap API. It renders the URL you give it and then answers in whichever of three ways you asked for.
The endpoint
https://api.websitescreenshotworld.com/instant-snap/v1/capturesSend Content-Type: application/json and your key in X-Api-Key. A successful capture answers 200; there is no 202 and no polling loop, so the connection stays open for as long as the page takes to render.
The smallest valid request
url is the only required field. Every other field has a default, and the defaults endpoint will tell you what they currently are.
curl -X POST https://api.websitescreenshotworld.com/instant-snap/v1/captures \
-H "X-Api-Key: $WSW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}'That gives you a 1920×1080 desktop PNG with advertising and pop-ups blocked, described by a JSON envelope.
Choosing how it answers
response_type is the field that changes the shape of the response, so decide it first.
| Value | Meaning |
|---|---|
json | Default. A JSON envelope with a signed download link. The capture is stored. |
by_format | The file bytes in the response body. Nothing is stored. |
cloud_storage | The file is written to your own bucket. The envelope describes the delivery instead of linking to our copy. |
response_type: json
The default, and the right choice when your code wants to know about the capture — its dimensions, how long it took, what HTTP status the page itself returned — as well as fetch the file.
{
"success": true,
"id": "0f9d1a6c-3b2e-4f18-8c6a-1d4b7e9a2c05",
"status": "succeeded",
"response_type": "json",
"media_category": "image",
"format": "png",
"content_type": "image/png",
"size_bytes": 184203,
"width": 1920,
"height": 1080,
"duration_ms": 3184,
"page_http_status": 200,
"captured_at": "2026-09-01T09:14:22.000Z",
"final_url": null,
"media_sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"download_url": "https://cdn.websitescreenshotworld.com/...",
"expires_at": "2026-09-02T09:14:22.000Z",
"page_source": null,
"s3_delivery": null,
"callback_url": null,
"error_code": null,
"spec_version": "v1"
}The link is short-lived, the file less so
download_url is signed and expires after 60 minutes. The capture itself is kept for 24 hours: read it back with GET /captures/{captureId} any time in that window and you get a fresh link. After 24 hours the media is deleted and the record reports status: expired.response_type: by_format
The response body is the file itself, with the metadata moved into response headers. Use it when you are piping the bytes somewhere directly and never want a second round trip.
curl -X POST https://api.websitescreenshotworld.com/instant-snap/v1/captures \
-H "X-Api-Key: $WSW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"response_type": "by_format",
"format": "webp"
}' \
--output homepage.webpHTTP/1.1 200 OK
Content-Type: image/webp
X-InstantSnap-Id: 0f9d1a6c-3b2e-4f18-8c6a-1d4b7e9a2c05
X-InstantSnap-Status: succeeded
X-InstantSnap-Width: 1920
X-InstantSnap-Height: 1080
X-InstantSnap-Size-Bytes: 96114
X-InstantSnap-Duration-Ms: 2914
X-InstantSnap-Page-Http-Status: 200
X-InstantSnap-Spec-Version: v1
X-InstantSnap-Captured-At: 2026-09-01T09:14:22.000Z
X-InstantSnap-Sha256: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
X-InstantSnap-Part-Count: 1Nothing is stored
by_format capture is not retained, so GET /captures/{captureId} will not find it later even though the response carries an id. If you need the file again, capture it again — or use json, which keeps it for 24 hours.An inline response is capped at 64 MiB. A capture larger than that answers 413 payload_too_large; ask for json or cloud_storage instead, neither of which has to fit in a response body.
A page-content capture answers with the text itself: text/markdown; charset=utf-8 or text/plain; charset=utf-8.
response_type: cloud_storage
The capture is written straight into an S3-compatible bucket you control, and the envelope reports where it landed. Requires an s3_delivery block in the request — Deliver to your S3 bucket covers the credentials, the key layout and what happens when the write fails.
The response envelope
| Field | Type | Default | Description |
|---|---|---|---|
success | boolean | — | Always true on a 200. |
id | uuid | — | The capture id. Use it with GET /captures/{captureId}. |
status | string | — | succeeded, failed or expired. See the status table below. |
response_type | string | — | Echoes back what you asked for, so a response is self-describing in a log. |
media_category | string | — | image, video, pdf or content. |
format | string | — | The format written, e.g. png, mp4 or markdown. |
content_type | string | — | The MIME type of the file, e.g. image/png. |
size_bytes | integer | — | Size of the produced file. |
width | integer | — | Pixel width of the result. Null for PDFs. |
height | integer | — | Pixel height of the result. With auto_height on a full-page capture this is the page height, not the viewport height. |
duration_ms | integer | — | How long the render took. Useful for tuning page_timeout_ms. |
page_http_status | integer | — | The status the captured page itself returned. A 200 capture of a 404 page is still a successful capture — check this field if the image looks wrong. |
captured_at | string (ISO 8601, UTC, milliseconds) | — | When the page was in front of the browser — the moment the render itself happened, not when the response was assembled. On a long video capture the two are most of a minute apart. |
final_url | string | — | Where the browser actually landed, if a redirect took it somewhere other than the url you sent. Null means there was no redirect — it is never a copy of your own url, so this field alone answers whether one happened. |
media_sha256 | string (64 hex characters) | — | SHA-256 of the file this capture produced. Run sha256sum over your download and you get the same string back. It stays on the record after the file itself has expired, so a capture you took months ago can still be matched against a copy you kept. |
download_url | string | — | Signed link, valid 60 minutes. Null once the capture has expired, and null on a cloud_storage response. |
expires_at | string (ISO 8601, UTC, milliseconds) | — | When the stored file is deleted: 24 hours after the capture. The download link itself is valid for 60 minutes and re-issued on every read. |
s3_delivery | object | — | Where the file was written, on a cloud_storage response. Null otherwise. |
callback_url | string | — | Echoes the value you sent, if any. The result is posted there - see the note below. |
error_code | string | — | Why a failed or timed-out capture ended, as one of the error codes. Null otherwise. |
spec_version | string | — | Which generation of the default set filled this capture in, e.g. v1. An old capture keeps its meaning when a default later changes. |
| Value | Meaning |
|---|---|
queued | An async capture accepted and waiting for a browser. Transient. |
running | An async capture a browser is working on now. Transient. |
pending | The render has not finished. Transient. |
succeeded | The page rendered and the file exists. |
delivered | A cloud_storage capture that reached your bucket. |
failed | The render did not produce a file. You normally meet this as an error response rather than a 200. |
timed_out | The page did not settle within page_timeout_ms. |
expired | The capture is older than 24 hours and the media has been deleted. |
Receiving the result at callback_url
callback_url, the finished capture is also posted there: the same capture.succeeded or capture.failed event, payload, X-WSW-Signature header and retries as the endpoints on your Webhooks page, signed with your account's signing secret. A cloud_storage capture also sends delivery.completed once its upload to your bucket has finished or finally failed. A request refused before the capture ran (capture_busy, service_unavailable, queue_full) sends nothing: its response is the answer. It needs a plan that includes webhooks; on any other plan the request is refused with 403 feature_not_available.Capturing the page source too
Add a page_source block and the same run saves the page's own markup beside the capture. Sending the block is the whole instruction:
{
"url": "https://example.com/pricing",
"response_type": "json",
"page_source": { "format": "html" }
}The answer carries a page_source block of its own, with a link that expires when the capture's does:
"page_source": {
"format": "html",
"content_type": "text/html",
"size_bytes": 48211,
"sha256": "3b1f8a0c5d9e2470ab6c1e83f5d2a7c40918e6b3d5a2f7c81b0e94d6a3f27c15",
"download_url": "https://cdn.websitescreenshotworld.com/..."
}It costs a second unit
quota_exhausted having captured nothing.json or cloud_storage only
by_format answers with the raw file as the body, so it has nowhere to put a second file. Sending page_source with it is a validation failure.A fuller example
A mobile, full-page capture of a pricing page, with the cookie banner dismissed and a timestamp burned into the corner:
{
"url": "https://example.com/pricing",
"response_type": "json",
"media_category": "image",
"format": "png",
"device_category": "mobile",
"emulated_device": "iPhone 13",
"scroll_mode": "scroll",
"auto_height": true,
"max_height": 12000,
"block_ads": true,
"block_pop_ups": true,
"page_timeout_ms": 45000,
"scrolling_delay_ms": 2500,
"locale": "en-GB",
"time_zone": "Europe/London",
"selectors": {
"hide": "#cookie-banner, .newsletter-modal",
"click": "#accept-all"
},
"timestamp_watermark": {
"enabled": true,
"location": "bottom"
}
}Every field here is described in Request parameters, including which of them your plan has to include.
Async captures
A json or cloud_storage capture does not need you to hold the connection open. Add "async": true and the API answers at once with 202 Accepted, a Location header and the capture with status: queued; the capture runs as soon as a browser is free. Read it back with GET /captures/{id} until it is no longer queued or running, or send a callback_url and receive the result as a webhook.
curl -i -X POST https://api.websitescreenshotworld.com/instant-snap/v1/captures \
-H "X-Api-Key: $WSW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/annual-report",
"media_category": "pdf",
"async": true,
"timeout": 240
}'HTTP/1.1 202 Accepted
Location: /instant-snap/v1/captures/0f9d1a6c-3b2e-4f18-8c6a-1d4b7e9a2c05
{
"success": true,
"id": "0f9d1a6c-3b2e-4f18-8c6a-1d4b7e9a2c05",
"status": "queued",
"response_type": "json",
"media_category": "pdf",
"format": "pdf",
"download_url": null,
"error_code": null,
...
}timeoutis the browser time the capture may use, every attempt included: 30–300 seconds, 120 when you leave it out. Time spent waiting in the queue does not count against it. It is accepted only withasync.- A capture that has not started within 15 minutes of being accepted ends
failedwithcapture_expiredand is not charged. - Up to 100 async captures per workspace may be waiting or running at once. Past that the request is refused with
429 queue_fulland nothing is queued or charged. - Everything that refuses a synchronous request — validation, key, plan, quota, the per-minute limit — refuses an async one the same way, before anything is queued. The charge happens when the capture succeeds, as it always does.
- An
Idempotency-Keyworks as for any capture, and a retry never queues it twice: it is answered202 Acceptedwith the capture'sLocationand its current state — queued, running, finished or failed — andIdempotent-Replayed: true. A capture that never ran (queue_full,capture_expired) releases the key, so the retry queues it again.
Not with by_format
by_format answers with the file itself, so there is nothing to come back for. Sending async with it is a validation failure; ask for json instead.Timing and timeouts
- The request is synchronous. Set your own HTTP client timeout above
page_timeout_msplus a margin, or your client will give up on a capture that is still working. Every answer, including await_for_syncdelivery, leaves within 2 minutes, so a client timeout of 130 seconds never cuts one off. The capture itself gets at most 100 seconds; one that could not be stored and answered within the 2 minutes ends as504 capture_timeoutand is not charged. For a page that needs longer, use an async capture: the request is answered at once and the capture may use up to 300 seconds. page_timeout_msdefaults to 30 000 and accepts 1 000–60 000. A page that never settles answers504 capture_timeout.- A full-page capture waits
scrolling_delay_msat each step, so a long page costs noticeably more wall-clock time than a viewport capture of the same site. 503 capture_busymeans the renderer is saturated, not that your request was wrong. Retry it after theretry_after_secondsthe error gives you.
Retrying safely
Send an Idempotency-Key header (up to 255 printable characters, a UUID is ideal) and a retry with the same key and the same body never captures or charges twice. Keys are kept for 24 hours per account. The official client libraries send one on every capture for you.
- If the first request finished, the retry gets its answer: the capture (with a fresh
download_url), or the same error it ended with. The response carriesIdempotent-Replayed: true. - If the first request is still running, the retry gets
409 idempotency_key_in_usewithretry_after_seconds; wait and send it again. - The same key with a different body, or for a
by_formatcapture (whose file is not kept to hand back), gets409 idempotency_key_reused. - A request refused before anything ran (validation, key, quota, a busy renderer) does not use up the key: retry it as it is.
Did this page answer your question?
Tell us what is missing