Async and webhooks
202, signed callbacks, retries and deduplication.
async=true returns immediately with 202 Accepted and a request id:
{ "is_successful": true, "request_id": "req_01...", "status": "accepted" }
Pair it with webhook_url to be told when the file is ready. An async request with response_type=by_format and neither a webhook nor storage is rejected — the result would have nowhere to go.
Verifying a webhook
X-PageCapture-Event-Id: evt_01...
X-PageCapture-Timestamp: 1785520800
X-PageCapture-Signature: v1=<hex>
X-PageCapture-Attempt: 1
The signed content is timestamp + "." + raw_body, with HMAC-SHA256 under your signing secret. Verify the signature against the raw body, before parsing.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody: string, headers: Headers, secret: string) {
const timestamp = headers.get("x-pagecapture-timestamp")!;
const received = headers.get("x-pagecapture-signature")!;
const expected = "v1=" + createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
// Reject anything older than five minutes.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(received));
}
Retries
Delivery is retried immediately, then after 30 s, 2 min, 10 min, 1 h, 6 h and 24 h, with jitter. Every attempt carries the same X-PageCapture-Event-Id and an incrementing X-PageCapture-Attempt, so your endpoint can deduplicate on the event id.
Any 2xx is success. A 410 Gone stops delivery permanently.
A webhook that never succeeds does not refund the credit: the capture was produced and remains retrievable through GET /v1/requests/{id} for its retention window.