API docs · Tasks
Webhooks & signatures
Event format, X-Signature verification and delivery retries.
Pass a callback_url when creating a task, and when the task reaches a terminal status the platform sends a POST with an event: task.succeeded or task.failed. The event's data is the full task object, identical to what the task endpoint returns.
Signing secret
Generate a webhook signing secret under Console · Account settings. The secret is shown only once. After you rotate it, the old and new secrets are both valid for 24 hours so you can switch over without downtime.
Verifying signatures
Once you have generated a signing secret, every callback request carries a signature header. Without a secret, callbacks are sent unsigned, so always generate one and verify signatures in production:
X-Signature: t=<unix seconds>,v1=<hex>
where v1 = hex(HMAC_SHA256(secret, t + "." + raw_body)). To verify:
- Parse
tand everyv1from the header: for 24 hours after a secret rotation, the header carries twov1values (new and old), and a match on either one passes. - Concatenate
t + "." + bodyusing the raw, unparsed request body and compute HMAC-SHA256. - Compare the result with each
v1using a constant-time comparison. - Check that
tis within 5 minutes of the current time to prevent replay attacks.
Delivery and retries
- Any 2xx response counts as a successful delivery. Each request times out after 10 seconds, so respond quickly and move slow work to a queue. Redirects (3xx) are not followed and count as failures.
- If the first delivery fails (non-2xx or timeout), it is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, then dropped. The task result is unaffected and can still be fetched.
callback_urlmust be a publicly reachable http / https URL.- The same event may be delivered more than once; deduplicate by
id(evt_…). - Events may arrive out of order; treat the task object's status as the source of truth.
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.AITOKEN_WEBHOOK_SECRET;
// Verify against the raw request body — never JSON.parse and re-serialize first
app.post("/hooks/video", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-Signature") ?? ""; // t=1730259000,v1=5f2c...[,v1=old-secret signature]
const pairs = header.split(",").map((kv) => kv.split("="));
const t = Number(pairs.find(([k]) => k === "t")?.[1]);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.status(400).end(); // 5-minute window
const expected = Buffer.from(
crypto.createHmac("sha256", SECRET).update(`${t}.${req.body.toString("utf8")}`).digest("hex"),
);
// For 24 hours after a secret rotation, two v1 values (new and old) are sent; either match passes
const ok = pairs
.filter(([k, v]) => k === "v1" && v?.length === expected.length)
.some(([, v]) => crypto.timingSafeEqual(Buffer.from(v), expected));
if (!ok) return res.status(400).end();
const event = JSON.parse(req.body.toString("utf8"));
// Deduplicate by event.id; return 2xx quickly and queue slow work
res.status(204).end();
});