POST /api/jobs

Queue the capture. Get the result when it is ready.

Async screenshots accept the same request body as the synchronous API, store the result, retry failed attempts, and tell you when the job finishes, by polling or a signed webhook.

Choose a mode

Synchronous, async or batch?

ModeEndpointPlansUse it for
SynchronousPOST /api/screenshotAll plansOne capture that finishes quickly, where the caller needs the bytes in the same response.
Async jobPOST /api/jobsBasic and aboveSlow pages, PDFs, captures triggered by user actions, or anything you want retried without holding a connection open.
BatchPOST /api/jobs/batchGrowth and ScaleUp to 20 validated captures queued in one request, each tracked as its own job.

Rule of thumb: if a slow capture would delay your user, risk a client or platform timeout, or need a retry you would otherwise write yourself, queue it. Quotas and plan gates are on the pricing page.

How it works

Durable from request to download.

1. Queue

Send the same JSON body you would send to the synchronous endpoint. A new job returns 202 with an id, a statusUrl and a Location header after the encrypted payload is stored.

2. Capture

A worker claims the job and renders it in Chromium. A failed attempt is retried with exponential backoff, up to three attempts in total.

3. Store

The result is kept for your account retention period, 1 to 30 days (7 by default). Reading the job returns a download URL signed for about 15 minutes.

4. Notify

Poll GET /api/jobs/{id}, or attach a webhook. Webhooks are signed with HMAC-SHA256 when you set a secret and get up to five delivery attempts.

Submit

Retry the request, not the capture.

An Idempotency-Key makes a network retry return the original job (200, replayed: true) instead of queueing a duplicate.

curl -i -X POST https://screenshotly.app/api/jobs \
  -H "x-api-key: $SCREENSHOTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: invoice-2026-11-0042" \
  -d '{
    "url": "https://example.com/invoices/0042",
    "format": "pdf",
    "webhook": {
      "url": "https://app.example.com/hooks/screenshotly?ref=invoice-0042",
      "secret": "whsec_replace_me"
    }
  }'

# 202 Accepted
# Location: /api/jobs/<job id>
# { "id": "<job id>", "status": "QUEUED", "replayed": false, "statusUrl": "/api/jobs/<job id>" }

Poll

Read status at any time.

Polling works without a public endpoint and is the fallback when a webhook delivery fails.

curl https://screenshotly.app/api/jobs/<job id> \
  -H "x-api-key: $SCREENSHOTLY_API_KEY"

# { "id": "...", "status": "SUCCEEDED", "attempts": 1, "maxAttempts": 3,
#   "asset": { "contentType": "application/pdf", "size": ..., "expiresAt": "..." },
#   "webhookDeliveries": [{ "status": "DELIVERED", "attempts": 1, ... }],
#   "resultUrl": "https://..." }
QUEUED
Accepted and waiting for a worker, or waiting for its next retry.
RUNNING
A worker is rendering the page.
SUCCEEDED
The file is stored. The response includes asset metadata and a resultUrl.
FAILED
All attempts were used. errorCode explains why, for example CAPTURE_FAILED or WORKER_TIMEOUT.
CANCELED
Canceled with DELETE /api/jobs/{id} before a result was stored.

Webhooks

Signed callbacks, five attempts.

  • Events: screenshot.completed with a 24-hour storageUrl, and screenshot.failed after the final attempt.
  • Signature: X-Screenshotly-Signature: sha256=<hex>, an HMAC-SHA256 of the raw body, sent when webhook.secret is set.
  • Delivery: a 2xx within 10 seconds succeeds. Redirects and timeouts are failures and are retried with backoff, up to five attempts.
  • Targets: public http or https URLs only. Private networks and localhost are rejected.

Verification code and retry handling: screenshot webhooks guide and the webhooks docs guide.

Batch · Growth and Scale

Up to 20 captures per request.

Every item is validated before anything is queued. Each capture becomes its own job with its own status URL.

curl -X POST https://screenshotly.app/api/jobs/batch \
  -H "x-api-key: $SCREENSHOTLY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: catalog-refresh-2026-11-13" \
  -d '{
    "captures": [
      { "url": "https://example.com", "format": "png" },
      { "url": "https://example.com/pricing", "format": "webp", "fullPage": true }
    ]
  }'

# { "jobs": [ { "id": "...", "status": "QUEUED", "replayed": false, "statusUrl": "/api/jobs/..." }, ... ] }

Patterns for larger runs: batch screenshot processing.

Limitations

What async capture does not do.

  • Async jobs and webhooks require the Basic plan or higher. Batch requires Growth or Scale. The Free plan and the public playground can only make synchronous captures.
  • A batch accepts 1 to 20 captures. If any item is invalid, unsafe or above your plan, the whole batch is rejected and nothing is queued.
  • A batch must also fit in your remaining monthly allowance; otherwise it returns 429 before queueing anything.
  • Every capture option still follows its own plan gate. For example, PDF output needs Basic and device mockups need Growth.
  • Capture retries are fixed at three attempts and webhook deliveries at five. Neither count is configurable per request.
  • Webhook payloads do not include the job id. Put your own reference in the webhook URL or webhook headers.
  • A webhook that fails all five attempts is not resent. Read the job to recover the result.
  • There is no bulk status endpoint. GET /api/jobs returns the 50 most recent jobs in the workspace; read individual jobs for anything older.
  • Canceling a running job is a request. The browser may finish its current step before the worker discards the output.
  • Stored results are deleted when the retention period ends. Copy files you need to keep into your own storage.
  • PDF output is A4 with 1 cm margins. Page size, orientation and headers or footers cannot be changed.

FAQ

Async screenshot questions.

When should I use async jobs instead of the synchronous endpoint?
Use async jobs when a capture may be slow, when it is triggered by a user and should not block their request, or when you want server-side retries and a stored result. For quick one-off captures where you need the bytes immediately, the synchronous endpoint is simpler.
How does Idempotency-Key work for async screenshots?
Send an Idempotency-Key header of up to 200 printable ASCII characters with POST /api/jobs. Repeating the request with the same key in the same workspace returns the original job with status 200 and replayed: true instead of queueing a second capture. For batches the key is suffixed with each capture position, so it must be 180 characters or fewer.
Do failed or retried captures use my quota?
Only successful captures count toward your monthly allowance. Server-side retry attempts that fail do not consume captures, and a job that ends in FAILED does not consume one either.
How do I verify a Screenshotly webhook?
Set webhook.secret on the capture. Each delivery then includes X-Screenshotly-Signature: sha256=<hex>, an HMAC-SHA256 of the raw request body. Recompute it with your secret and compare in constant time.
How long can I download an async result?
The file is retained for your account retention period, 1 to 30 days with a default of 7. The resultUrl returned by GET /api/jobs/{id} is signed for about 15 minutes, so read the job again for a fresh link. The storageUrl in a completed webhook is valid for 24 hours, within the same retention period.