Webhooks
Register an HTTPS endpoint and we POST to it the moment a job reaches a final state. Every delivery is signed. Verify the signature before trusting the payload.
Setup
Add your endpoint at Dashboard → Webhooks. Each endpoint gets its own signing secret, shown at creation. Deliveries are sent for these events:
| Parameter | Type | Description |
|---|---|---|
| job.succeeded | event | The job finished and the result is downloadable. |
| job.failed | event | The engine gave up; credits were refunded. |
| job.cancelled | event | The job was cancelled before finishing. |
The delivery
Deliveries are JSON POSTs. Respond with any 2xx to acknowledge. Anything else (including a timeout) is retried with backoff.
Delivery payload
{
"id": "evt_8f2ka91m",
"type": "job.succeeded",
"created_at": "2026-07-25T09:30:04Z",
"data": {
"id": "cmrw0iqm8000bdcs3um6ii7fa",
"object": "job",
"status": "succeeded",
"target": "4k",
"credits": 3,
"result": {
"bytes": 8412930,
"content_type": "image/png",
"url": "/api/v1/jobs/cmrw0iqm8000bdcs3um6ii7fa/result"
},
"created_at": "2026-07-25T09:30:00Z",
"finished_at": "2026-07-25T09:30:04Z"
}
}Verify the signature
Each delivery carries an X-Upscalr-Signature header of the form t=<unix>,v1=<hmac>. The HMAC is SHA-256 over <timestamp>.<raw body> using your endpoint secret. Verify against the raw request bytes (parsing and re-serialising the JSON first will change them), and reject deliveries older than five minutes to block replays.
import crypto from "node:crypto";
export function verify(header, rawBody, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
// Reject replays: the timestamp is part of the signed material.
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}Good practice
- Acknowledge fast: return 2xx first, then do slow work (like downloading the result) out of band. A handler that takes too long looks like a failure and gets retried.
- Deliveries can arrive more than once, so treat the event
idas an idempotency key on your side. - Don’t rely on ordering. If two events race, fetch the job for its current state instead of trusting the older payload.