Async, bulk & webhooks

For high-throughput workloads (hundreds or thousands of screenshots), use the async endpoints.

Async single screenshot

POST /v1/screenshot-async — same body as /v1/screenshot, but always returns 202 Accepted immediately.

curl https://websitescreenshotapi.net/api/v1/screenshot-async \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://news.ycombinator.com",
    "format": "png",
    "full_page": true,
    "webhook_url":    "https://yoursite.com/wh/screenshot",
    "webhook_secret": "my-shared-secret-32-chars"
  }'

Response:

{
  "job_id":             "job_abc",
  "db_id":              42,
  "status":             "queued",
  "status_url":         "https://websitescreenshotapi.net/api/v1/screenshot/42",
  "credits_charged":    1,
  "credits_remaining":  1999,
  "webhook_configured": true
}

Then either:

Bulk batch (up to 500 URLs)

POST /v1/bulk — fire-and-forget batch of URLs with shared options.

curl https://websitescreenshotapi.net/api/v1/bulk \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "urls": [
      "https://example.com",
      "https://news.ycombinator.com",
      "https://github.com"
    ],
    "format":         "png",
    "viewport_width": 1280,
    "block_ads":      true,
    "webhook_url":    "https://yoursite.com/wh/batch",
    "webhook_secret": "my-shared-secret"
  }'

Response:

{
  "batch_id":           "bat_a1b2c3d4e5f6...",
  "status":             "queued",
  "total":              3,
  "credits_charged":    3,
  "credits_remaining":  1997,
  "webhook_configured": true,
  "status_url":         "https://websitescreenshotapi.net/api/v1/batch/bat_..."
}

Credits are debited upfront (worst case if you mix formats; for now homogeneous batches only).

Get batch status

GET /v1/batch/{batch_id} — progress + completion.

{
  "batch_id":   "bat_a1b2…",
  "status":     "processing",
  "total":      3,
  "processed":  2,
  "failed":     0,
  "progress":   0.667,
  "created_at": "2026-05-06T01:00:00+00:00",
  "started_at": "2026-05-06T01:00:01+00:00",
  "has_webhook":true,
  "results":    null
}

Add ?include=results to get all individual results inline:

GET /v1/batch/bat_a1b2…?include=results
{
  "...": "...",
  "results": [
    {
      "db_id": 42,
      "input_url": "https://example.com",
      "url": "https://websitescreenshotapi.net/files/123/job_abc.png",
      "expires_at": "2026-05-07T01:00:00+00:00",
      "format": "png",
      "status": "done",
      "size_bytes": 17117,
      "width": 1280,
      "height": 720,
      "duration_ms": 412
    }
  ]
}

Webhooks

When you supply webhook_url (and optional webhook_secret), we POST a JSON body to that URL when the work is done.

Headers

POST /your-webhook HTTP/1.1
Content-Type: application/json
User-Agent: ScreenshotAPI-Webhook/1.0
X-ScreenshotAPI-Event: screenshot.completed
X-ScreenshotAPI-Signature: sha256=<hex>

Events

Payload — screenshot.completed

{
  "event": "screenshot.completed",
  "screenshot": {
    "job_id":          "job_…",
    "db_id":           42,
    "input_url":       "https://example.com",
    "url":             "https://websitescreenshotapi.net/files/123/job_abc.png",
    "expires_at":      "2026-05-07T01:00:00+00:00",
    "format":          "png",
    "status":          "done",
    "size_bytes":      17117,
    "width":           1280,
    "height":          720,
    "duration_ms":     412,
    "credits_charged": 1,
    "error_code":      null,
    "error_message":   null,
    "created_at":      "2026-05-06T01:00:00+00:00",
    "completed_at":    "2026-05-06T01:00:01+00:00"
  },
  "sent_at": "2026-05-06T01:00:01+00:00"
}

Payload — batch.completed

{
  "event":         "batch.completed",
  "batch_id":      "bat_a1b2…",
  "status":        "completed",
  "total":         3,
  "processed":     3,
  "failed":        0,
  "created_at":    "2026-05-06T01:00:00+00:00",
  "completed_at":  "2026-05-06T01:00:30+00:00",
  "results":       [ /* array of screenshot objects */ ],
  "sent_at":       "2026-05-06T01:00:31+00:00"
}

Verify the signature

If you supplied webhook_secret, verify the signature with timing-safe HMAC-SHA256:

Node.js:

import crypto from 'node:crypto';

export function verify(rawBody, sigHeader, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret)
                                     .update(rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(sigHeader), Buffer.from(expected));
}

Python:

import hmac, hashlib

def verify(raw_body: bytes, sig: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

PHP:

function verify(string $rawBody, string $sig, string $secret): bool {
    $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
    return hash_equals($expected, $sig);
}

Retry policy

We retry 3 times with exponential backoff (1s, 4s, 16s) on non-2xx responses. After 3 failures, the webhook is abandoned. The result remains accessible via GET /v1/screenshot/{db_id} or GET /v1/batch/{id}.

The full body is the raw POST body — don't re-serialize before HMAC verification.