Instant Snap API
Deliver to your S3 bucket
Set response_type to cloud_storage and the capture is written straight into storage you own. Nothing has to be configured in the application first — the destination travels with the request.
Why deliver to your own bucket
- Our retention does not apply. A capture we host is deleted after 24 hours; a capture in your bucket is yours for as long as you keep it.
- No second round trip — you never have to fetch the file to store it.
- The file lands inside your own compliance boundary, in your region, under your lifecycle rules.
Any S3-compatible endpoint works through the same fields: AWS S3, MinIO, Cloudflare R2, Wasabi, Backblaze B2 and DigitalOcean Spaces have all been used with it.
The request
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": "cloud_storage",
"s3_delivery": {
"access_key_id": "AKIAIOSFODNN7EXAMPLE",
"secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"bucket": "my-captures",
"region": "eu-central-1",
"key_prefix": "reports/2026-09"
}
}'The block belongs to one mode only
s3_delivery is required for response_type: cloud_storage and rejected for json and by_format. Sending it with the wrong mode is a validation failure, not a field the API ignores.The s3_delivery block
| Field | Type | Default | Description |
|---|---|---|---|
access_key_id | string | — | The access key id. Echoed back masked to its last four characters, never in full. |
secret_access_key | string | — | The secret. Write-only: never returned by any endpoint, never logged, and erased from storage as soon as the upload reaches a terminal state. |
bucket | string | — | The destination bucket. |
region | string | — | e.g. eu-central-1. At least one of region or endpoint_url must be given. |
endpoint_url | string | — | A custom S3-compatible endpoint. Must be an absolute https URL that does not resolve to a private address. |
force_path_style | boolean | false | Address the bucket as a path rather than as a subdomain — what MinIO and most S3-compatible gateways need. |
key_prefix | string | instant-snap/YYYY/MM | The prefix the object is written under. Normalised to one shape: no leading or trailing slash, no empty segments. |
wait_for_sync | boolean | false | Block the call until the upload reaches a terminal state. See below. |
A missing field here has its own error code
s3_destination_invalid rather than validation_failed, so you are told which half of the request is wrong without having to guess.How credentials are handled
secret_access_keyis write-only. No endpoint returns it, it is kept out of logs, and it is erased from storage once the upload has succeeded or finally failed.access_key_idcomes back masked — the last four characters only — so a receipt is readable in a log without being useful to anyone who finds it.- Nothing is stored between requests. Credentials travel with each call, which is what makes the destination self-contained.
Issue a write-only key
s3:PutObject on one prefix of one bucket. A key scoped that narrowly is worth very little to anyone who obtains it, and costs you nothing here.Endpoint rules
A custom endpoint_url asks this service to make a request on your behalf, so it is checked before it is called:
- It must be an absolute URL, and it must use https.
- It must not resolve to a loopback or private address —
127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16and their IPv6 equivalents are all refused.
Whether the bucket exists and whether these credentials may write to it cannot be checked in advance — those surface as a delivery failure rather than a validation error.
Where the file lands
{key_prefix}/{captureId}/{fileName}A page source arrives as a second object
page_source delivers two objects under the same prefix: the capture itself, and page-source.html beside it. The s3_delivery.status in the response tracks the capture — the source is uploaded on its own and never holds the response open, so wait_for_sync waits for the capture only. That also means a page-source upload that fails is retried and logged but does not show up in s3_delivery.status; the source stays available from its own download_url for the usual 24 hours either way.The capture id is always part of the key, so two captures never overwrite each other however you prefix them. Omit key_prefix and the same year/month layout our own storage uses is applied — instant-snap/2026/09/… — so a customer bucket reads the way ours does.
- Leading and trailing slashes are stripped and empty segments removed.
- A prefix containing
..is refused rather than rewritten. A prefix is a key prefix, not a path, so a traversal attempt is a mistake worth refusing.
wait_for_sync
Off by default. The capture request returns as soon as the render is done, and the upload continues behind it — a slow bucket should not slow a synchronous API down. You can poll the outcome from GET /captures/{captureId}.
| Value | Meaning |
|---|---|
wait_for_sync: false | Default. Fastest response. s3_delivery.status will usually read not_started or in_progress when the call returns. |
wait_for_sync: true | The call blocks until the upload has completed or failed, so the response is a final answer. Slower, and it counts against your own client timeout. |
The delivery receipt
{
"success": true,
"id": "0f9d1a6c-3b2e-4f18-8c6a-1d4b7e9a2c05",
"status": "succeeded",
"response_type": "cloud_storage",
"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": null,
"expires_at": "2026-09-02T09:14:22.000Z",
"page_source": null,
"s3_delivery": {
"status": "completed",
"bucket": "my-captures",
"key": "reports/2026-09/0f9d1a6c-3b2e-4f18-8c6a-1d4b7e9a2c05/capture.png",
"access_key_id": "****MPLE",
"error": null
},
"callback_url": null,
"error_code": null,
"spec_version": "v1"
}download_url is null on a cloud_storage response — the file is in your bucket, and issuing a link to our copy would be misleading. Read the location from s3_delivery.bucket and s3_delivery.key instead.
| Value | Meaning |
|---|---|
not_started | Queued. The upload has not begun. |
in_progress | The upload is running. |
completed | The object is in your bucket at the key shown. |
failed | The upload did not succeed. s3_delivery.error says why. |
When the bucket refuses
A failed upload is not a failed capture
200, with s3_delivery.status: failed and a reason in s3_delivery.error. Code that checks only the HTTP status will believe a file reached your bucket when it did not.The usual causes, in the order they are worth checking:
- The credentials cannot write to that prefix — check the bucket policy, not just the key.
- The bucket does not exist in the region given, or is named differently.
- The provider needs path-style addressing and
force_path_stylewas left off. regionandendpoint_urldisagree — the endpoint wins, and a mismatched region signature is rejected by the provider.
Provider notes
AWS S3 needs only region. Everything else needs endpoint_url, and most also need force_path_style:
{
"response_type": "cloud_storage",
"url": "https://example.com",
"s3_delivery": {
"access_key_id": "minioadmin",
"secret_access_key": "…",
"bucket": "captures",
"endpoint_url": "https://minio.example.com",
"force_path_style": true
}
}- Cloudflare R2 — endpoint
https://<account>.r2.cloudflarestorage.com, regionauto. - MinIO — your own https endpoint,
force_path_style: true. It must be reachable from the internet; a private address is refused. - Backblaze B2, Wasabi, DigitalOcean Spaces — the provider’s regional S3 endpoint, plus its region name.
Did this page answer your question?
Tell us what is missing