Webhooks
Instead of asking whether a job is done, let us tell you. We POST an event to your URL each time a job changes:
| Event | When |
|---|---|
job.processing |
The job left the queue and started. |
job.succeeded |
Every output is ready. |
job.failed |
It failed: data.error says why. |
job.canceled |
It was canceled. |
Events go to every endpoint of your organization (add them in the dashboard, under Webhooks) and to the job’s own webhook_url, if it has one.
POST /hooks/iomza HTTP/1.1Content-Type: application/jsonUser-Agent: Iomza-Webhooks/1.0webhook-id: 0199a0e3-9c1a-7f02-b6d8-3e7a4c0f5b91webhook-timestamp: 1793462400webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4={ "id": "0199a0e3-9c1a-7f02-b6d8-3e7a4c0f5b90", "object": "event", "type": "job.succeeded", "created_at": "2026-10-30T18:04:11.000Z", "data": { "id": "0199a0e1-…", "object": "job", "status": "succeeded", "outputs": { "…": "…" } }}data is the job, as GET /v1/jobs/{id} shows it (without billed_minutes).
Answer quickly, with a 2xx
Section titled “Answer quickly, with a 2xx”Reply with any 2xx within 10 seconds, then do the work. Anything else (another status, a redirect, a timeout) counts as a failed attempt, and the event is sent again after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h. You can see every delivery and its outcome, and send one again, in the dashboard, or list them:
GET /v1/webhook-deliveries?job_id={job id}Verify the signature
Section titled “Verify the signature”Webhooks follow Standard Webhooks. Each request is signed with your organization’s secret (whsec_…, in the dashboard under Webhooks), so you can check it came from us and wasn’t altered:
- Take the
webhook-idandwebhook-timestampheaders and the raw body, exactly as received (before any JSON parsing). - Compute HMAC-SHA256 of
<webhook-id>.<webhook-timestamp>.<body>, keyed with the base64-decoded part of the secret afterwhsec_. - Compare it, base64-encoded, with the signature after
v1,inwebhook-signature, in constant time. - Reject timestamps more than 5 minutes away from your clock, so a captured request can’t be replayed later.
The official standardwebhooks libraries do all of it:
import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.IOMZA_WEBHOOK_SECRET); // "whsec_…"
app.post("/hooks/iomza", express.raw({ type: "application/json" }), (req, res) => { let event; try { event = wh.verify(req.body, req.headers); // throws if the signature or timestamp is wrong } catch { return res.sendStatus(400); } res.sendStatus(204); handle(event); // after answering});Or by hand, in Node:
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, headers, rawBody) { const id = headers["webhook-id"]; const timestamp = Number(headers["webhook-timestamp"]); if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false; const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest(); return headers["webhook-signature"].split(" ").some((s) => { const given = Buffer.from(s.replace(/^v1,/, ""), "base64"); return given.length === expected.length && timingSafeEqual(given, expected); });}Rotating the secret in the dashboard replaces it at once: deliveries from then on are signed with the new one.
Expect repeats and disorder
Section titled “Expect repeats and disorder”Delivery is at least once: a network failure after you answered can make us send an event again. Keep the event ids you have handled and skip repeats. Events may also arrive out of order (a retried job.processing after job.succeeded, say): trust the job’s status in data, or fetch the job, rather than the order of arrival.
Requirements for your endpoint
Section titled “Requirements for your endpoint”- https, on a public address: private and internal addresses are refused, and checked again on every delivery.
- Answer the
POSTitself: redirects are not followed.