Async and webhooks
Queue renders in the background and receive the result as a signed POST to your webhook.
Queue a render
With async=true, the request returns 202 Accepted right away and the render runs in the background. When it's done, the result is POSTed to webhook_url:
curl -X POST "https://shotkit.net/api/take" \
-H "X-Access-Key: YOUR_ACCESS_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com","full_page":true,"async":true,"webhook_url":"https://your.app/hooks/shot","external_identifier":"page-42"}' \
--fail-with-body -o shot.jpg{ "is_successful": true, "status": "accepted" }Async requests always render; they don't read or write the cache. Use async for slow pages, long recordings, and batches where you don't want to hold connections open. timeout can go up to 600 seconds with async (90 without).
The monthly quota is checked when you queue the request. Your plan's per-minute rate limit applies to queued requests too.
The webhook
shotkit POSTs the same response a synchronous request would have returned:
- With the default
response_type, the body is the file andContent-Typeits media type. - With
response_type=json, the body is the JSON withscreenshot_urland metadata. This is the easiest to handle, and the only way to get metadata with async.
Headers on the delivery:
| Header | Value |
|---|---|
Content-Type |
The file's media type, or application/json. |
X-Signature |
Hex HMAC-SHA256 of the raw body, keyed with your secret key. Omitted with webhook_sign=false. |
X-External-Identifier |
Your external_identifier, if you set one. |
Respond with any 2xx within 15 seconds. Deliveries aren't retried, so store the body first and process it later.
Errors
By default, failed renders aren't delivered. With webhook_errors=true, the error JSON is POSTed instead, signed the same way:
{
"is_successful": false,
"error_code": "timeout_error",
"error_message": "Navigation timed out."
}Temporary failures are retried automatically before an error is sent: when all browsers are busy, every 15 seconds for up to an hour; after an infrastructure error, up to three times. Errors caused by the request or the page, such as an invalid selector or a 404, fail right away.
Verify the signature
Compute the HMAC-SHA256 of the raw request body (before any JSON parsing) with your secret key and compare it with X-Signature in constant time:
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const app = express();
app.post("/hooks/shot", express.raw({ type: "*/*", limit: "50mb" }), (req, res) => {
const expected = createHmac("sha256", process.env.SHOTKIT_SECRET_KEY).update(req.body).digest("hex");
const given = req.get("x-signature") ?? "";
if (given.length !== expected.length || !timingSafeEqual(Buffer.from(given), Buffer.from(expected))) {
return res.sendStatus(401);
}
// req.body is the file (or JSON), req.get("x-external-identifier") is your id.
res.sendStatus(204);
});