Async Jobs & Webhooks

Add async=true to any /v1/take request and the API answers at once with 202 and a job, instead of waiting for the render. The page is rendered in the background, with up to 300 seconds for slow pages, and you poll the job or receive a signed webhook when it is done.

Create a job

Send the request you would send without async, with async=true (or "async": true in a POST body). Every attribute works as in a sync request: formats, PDF and markdown options, HTML input and upload.

Create a job

GET
/v1/take?async=true
curl -G https://api.screenshotbase.com/v1/take \
    -H "apikey: YOUR-API-KEY" \
    -H "Idempotency-Key: order-42-screenshot" \
    --data-urlencode "url=https://example.com" \
    -d full_page=1 \
    -d async=true \
    --data-urlencode "webhook_url=https://example.com/webhooks/screenshotbase"

202 Accepted

{
    "job_id": "631442883836252160",
    "status": "queued",
    "format": "png",
    "external_identifier": null,
    "quota": { "charged": 1, "refunded": 0 },
    "result": null,
    "error": null,
    "webhook": { "status": "pending" },
    "created_at": "2026-10-09T12:00:00+00:00",
    "started_at": null,
    "finished_at": null,
    "expires_at": "2026-10-16T12:00:00+00:00",
    "links": {
        "self": "https://api.screenshotbase.com/v1/jobs/631442883836252160",
        "result": null
    }
}

The Location header of the 202 is the job's URL. What a sync request checks before it renders is checked when the job is created: the parameters, the target URL, your quota and the webhook_url, so those errors come back at once and nothing is billed.

Parameters

  • Name
    async
    Type
    boolean
    Description

    Set to 1 (or true) to answer at once with 202 and a job instead of the result. Every other attribute of GET /v1/take and POST /v1/take works as in a sync request. Default value is 0.

  • Name
    webhook_url
    Type
    string
    Description

    Only with async: an https URL of a public host that receives a signed POST when the job is completed or failed (see webhooks). Maximum 2048 characters.

  • Name
    external_identifier
    Type
    string
    Description

    Only with async: your own reference in printable ASCII, returned with the job, in its webhook and as the X-Screenshotbase-External-Identifier header of the webhook. Maximum 255 characters.

  • Name
    Idempotency-Key
    Type
    header
    Description

    A request header, not a parameter: 1 to 255 printable ASCII characters without spaces. For 24 hours a request with the same key answers with the job the first one created (Idempotent-Replayed: true, not billed again); the same key with another request answers 422, and while the first request is still being answered, 409.

Get the result

Poll GET /v1/jobs/{job_id} (free; about once a second at most) until status is completed or failed, or wait for the webhook. Then download the result from GET /v1/jobs/{job_id}/result: the same answer the request without async would have given, the image, the PDF, the markdown or, with upload=true, the JSON with the stored file's url. Before the job is completed it answers 409 with the error code job_not_completed.

cURL

curl https://api.screenshotbase.com/v1/jobs/631442883836252160 -H "apikey: YOUR-API-KEY"
curl https://api.screenshotbase.com/v1/jobs/631442883836252160/result -H "apikey: YOUR-API-KEY" -o example.png

A failed job carries error, what the request without async would have answered, for example {"status": 502, "code": "target_unreachable", "message": "..."}, and quota.refunded.

EndpointWhat it does
GET /v1/jobsYour jobs, newest first: limit (up to 100), before (the last job_id of the previous page) and status.
GET /v1/jobs/{job_id}The job: status (queued, processing, completed, failed), result, error, quota.
GET /v1/jobs/{job_id}/resultThe result, kept for 7 days after the job finished (expires_at).
DELETE /v1/jobs/{job_id}Deletes the job and its result. A queued job is refunded; one that is rendering answers 202 and is deleted, refunded, when its render ends.

Jobs belong to your account and to the kind of key that created them: the jobs of a sandbox key are only visible with a sandbox key, the others only with a live key.

Webhooks

With webhook_url, the job is sent to that URL as a signed POST when it is completed (screenshot.completed) or failed (screenshot.failed). In the dashboard, under Webhooks (team owners and admins), you can also add endpoints that receive these events for every job, send a test event, see every delivery and send one again.

Webhook body

{
    "id": "whd_01ja2y6k7q9w4v8t5r3n1m0p2z",
    "event": "screenshot.completed",
    "created_at": "2026-10-09T12:00:07+00:00",
    "data": { "job_id": "631442883836252160", "status": "completed", "...": "the job as GET /v1/jobs/{job_id} shows it" }
}

Every delivery has these headers:

Header
X-Screenshotbase-Eventscreenshot.completed or screenshot.failed
X-Screenshotbase-DeliveryThe delivery's id, the same on every retry: use it to ignore a delivery you already handled.
X-Screenshotbase-TimestampUnix time of the attempt.
X-Screenshotbase-Signaturev1= and the hex HMAC-SHA256 of <timestamp>.<body> with your signing secret. For 24 hours after you rotate the secret, a second v1= signature with the previous one follows, separated by a comma.
X-Screenshotbase-External-IdentifierYour external_identifier, when the job has one.

The signing secret of webhook_url deliveries is shown, and can be rotated, on the Webhooks page of the dashboard; every endpoint you add there has its own. Check the signature against the raw body before you parse it, and refuse old timestamps:

Verify a webhook (Node.js)

import crypto from 'node:crypto'

function verify(rawBody, headers, secret) {
    const timestamp = headers['x-screenshotbase-timestamp']
    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false
    const expected = 'v1=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex')
    return headers['x-screenshotbase-signature'].split(',').some((signature) =>
        signature.trim().length === expected.length &&
        crypto.timingSafeEqual(Buffer.from(signature.trim()), Buffer.from(expected)))
}

Verify a webhook (PHP)

$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_SCREENSHOTBASE_TIMESTAMP'];
$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);
$valid = abs(time() - (int) $timestamp) <= 300 && array_filter(
    explode(',', $_SERVER['HTTP_X_SCREENSHOTBASE_SIGNATURE']),
    fn ($signature) => hash_equals($expected, trim($signature)),
) !== [];

Answer with any 2xx status within 10 seconds. Any other answer, a timeout or a redirect counts as failed, and the delivery is tried again after 10 seconds, 1 minute, 5 minutes, 30 minutes and 2 hours (6 attempts over about 2.6 hours). The URL must be https on port 443 of a public host; it is checked again before every attempt.

Idempotency

Send an Idempotency-Key header with every create, so that a retry after a network error does not create (and bill) a second job. For 24 hours, a request with the same key answers with the job the first one created, with Idempotent-Replayed: true and at no cost; the same key with a different request answers 422, and while the first request is still being answered, 409 with Retry-After: 1.

Limits

  • A job's render has 300 seconds from when it starts; timeout may be up to 295 and delay up to 30.
  • Your account may have its plan's renders at the same time times 10 jobs queued or rendering at once; past that, the create answers 429 concurrency_limit_exceeded (without Retry-After: send it again when one of your jobs is done) and is not billed. Each account's jobs are rendered one at a time for now; the others wait in the queue.
  • A job that cannot start within 15 minutes, because the renderers are busy, fails with 503 renderer_busy and is refunded.
  • Jobs, their results and their HTML input are deleted 7 days after the job finished.

Errors

Besides the errors of /v1/take, the job endpoints answer:

  • 404 not_found for a job that does not exist, belongs to another account or kind of key, was deleted, or whose result is no longer kept;
  • 409 job_not_completed for the result of a job that is not completed;
  • 409 idempotency_key_in_use while the first request with the same Idempotency-Key is being answered;
  • 403 forbidden for a key that belongs to no screenshotbase account.

AI agents (MCP)

On the MCP server, take and takePost accept async, and the tools listJobs, getJob, getJobResult and deleteJob work with jobs.