Documentation menu
This API no longer accepts new upscales. POST /v1/upscale answers 410 gone. Retrieving, listing, downloading and cancelling existing jobs still work. Image upscaling has moved to AI Suite.

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.