Menu

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 and Content-Type its media type.
  • With response_type=json, the body is the JSON with screenshot_url and 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);
});