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

POSThttps://api.websitescreenshotworld.com/instant-snap/v1/captures

Send 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.

ValueMeaning
jsonDefault. A JSON envelope with a signed download link. The capture is stored.
by_formatThe file bytes in the response body. Nothing is stored.
cloud_storageThe 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.

200 OK
{
  "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.

Ask for the bytes
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.webp
Response headers
HTTP/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: 1

Nothing is stored

A 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

Fields present on a successful json or cloud_storage response.
FieldTypeDefaultDescription
successboolean—Always true on a 200.
iduuid—The capture id. Use it with GET /captures/{captureId}.
statusstring—succeeded, failed or expired. See the status table below.
response_typestring—Echoes back what you asked for, so a response is self-describing in a log.
media_categorystring—image, video, pdf or content.
formatstring—The format written, e.g. png, mp4 or markdown.
content_typestring—The MIME type of the file, e.g. image/png.
size_bytesinteger—Size of the produced file.
widthinteger—Pixel width of the result. Null for PDFs.
heightinteger—Pixel height of the result. With auto_height on a full-page capture this is the page height, not the viewport height.
duration_msinteger—How long the render took. Useful for tuning page_timeout_ms.
page_http_statusinteger—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_atstring (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_urlstring—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_sha256string (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_urlstring—Signed link, valid 60 minutes. Null once the capture has expired, and null on a cloud_storage response.
expires_atstring (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_deliveryobject—Where the file was written, on a cloud_storage response. Null otherwise.
callback_urlstring—Echoes the value you sent, if any. The result is posted there - see the note below.
error_codestring—Why a failed or timed-out capture ended, as one of the error codes. Null otherwise.
spec_versionstring—Which generation of the default set filled this capture in, e.g. v1. An old capture keeps its meaning when a default later changes.
ValueMeaning
queuedAn async capture accepted and waiting for a browser. Transient.
runningAn async capture a browser is working on now. Transient.
pendingThe render has not finished. Transient.
succeededThe page rendered and the file exists.
deliveredA cloud_storage capture that reached your bucket.
failedThe render did not produce a file. You normally meet this as an error response rather than a 200.
timed_outThe page did not settle within page_timeout_ms.
expiredThe capture is older than 24 hours and the media has been deleted.

Receiving the result at callback_url

When you send 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:

Request
{
  "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:

Response fragment
"page_source": {
  "format": "html",
  "content_type": "text/html",
  "size_bytes": 48211,
  "sha256": "3b1f8a0c5d9e2470ab6c1e83f5d2a7c40918e6b3d5a2f7c81b0e94d6a3f27c15",
  "download_url": "https://cdn.websitescreenshotworld.com/..."
}

It costs a second unit

The page source is charged against your image allowance whatever it rides with, so this request spends two. The check happens before the page is opened: a request that would exceed your allowance is refused with 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:

Request body
{
  "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.

Request
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
  }'
202 Accepted
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,
  ...
}
  • timeout is 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 with async.
  • A capture that has not started within 15 minutes of being accepted ends failed with capture_expired and 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_full and 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-Key works as for any capture, and a retry never queues it twice: it is answered 202 Accepted with the capture's Location and its current state — queued, running, finished or failed — and Idempotent-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_ms plus a margin, or your client will give up on a capture that is still working. Every answer, including a wait_for_sync delivery, 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 as 504 capture_timeout and 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_ms defaults to 30 000 and accepts 1 000–60 000. A page that never settles answers 504 capture_timeout.
  • A full-page capture waits scrolling_delay_ms at each step, so a long page costs noticeably more wall-clock time than a viewport capture of the same site.
  • 503 capture_busy means the renderer is saturated, not that your request was wrong. Retry it after the retry_after_seconds the 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 carries Idempotent-Replayed: true.
  • If the first request is still running, the retry gets 409 idempotency_key_in_use with retry_after_seconds; wait and send it again.
  • The same key with a different body, or for a by_format capture (whose file is not kept to hand back), gets 409 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