Documentation menu

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:

ParameterTypeDescription
job.succeededeventThe job finished and the result is downloadable.
job.failedeventThe engine gave up; credits were refunded.
job.cancelledeventThe 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 id as 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.