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

Capture and deliver
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

FieldTypeDefaultDescription
access_key_idstring—The access key id. Echoed back masked to its last four characters, never in full.
secret_access_keystring—The secret. Write-only: never returned by any endpoint, never logged, and erased from storage as soon as the upload reaches a terminal state.
bucketstring—The destination bucket.
regionstring—e.g. eu-central-1. At least one of region or endpoint_url must be given.
endpoint_urlstring—A custom S3-compatible endpoint. Must be an absolute https URL that does not resolve to a private address.
force_path_stylebooleanfalseAddress the bucket as a path rather than as a subdomain — what MinIO and most S3-compatible gateways need.
key_prefixstringinstant-snap/YYYY/MMThe prefix the object is written under. Normalised to one shape: no leading or trailing slash, no empty segments.
wait_for_syncbooleanfalseBlock the call until the upload reaches a terminal state. See below.

A missing field here has its own error code

None of these fields carries a generic required-field check. A missing bucket or credential answers 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_key is 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_id comes 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

The credentials only ever need 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/16 and 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

Object key
{key_prefix}/{captureId}/{fileName}

A page source arrives as a second object

A capture that also asked for 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}.

ValueMeaning
wait_for_sync: falseDefault. Fastest response. s3_delivery.status will usually read not_started or in_progress when the call returns.
wait_for_sync: trueThe 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

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

ValueMeaning
not_startedQueued. The upload has not begun.
in_progressThe upload is running.
completedThe object is in your bucket at the key shown.
failedThe upload did not succeed. s3_delivery.error says why.

When the bucket refuses

A failed upload is not a failed capture

The render succeeded; only the delivery did not. The response is still a 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:

  1. The credentials cannot write to that prefix — check the bucket policy, not just the key.
  2. The bucket does not exist in the region given, or is named differently.
  3. The provider needs path-style addressing and force_path_style was left off.
  4. region and endpoint_url disagree — 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:

MinIO, R2, Wasabi, Spaces
{
  "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, region auto.
  • 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