Pular para o conteúdo
UV pdfTool
Contents

Developer API

Webhooks

How the UV pdfTool API tells your server that a job ended: the events, the signed body, how to check it, and the retries.

Last updated 2026-10-29 · Written in English

Events

Set one HTTPS address on the API keys page, and every job made with your keys that ends is posted there as JSON. The address gets a signing secret (whsec_…), shown when it is set and again on request.

Events
NameTypeDescription
job.succeededeventThe job is done; its outputs are listed (names and sizes, no links).
job.failedeventThe job failed; data.job.error says why.
job.cancelledeventThe job was cancelled with DELETE, or never got its files.
pingeventA test event, sent when you press “Send a test event” on the keys page; data is empty.
The body
{
  "id": "8c1d2f30-5a7e-4b7f-8a1c-2e6b9d4f1a22",
  "type": "job.succeeded",
  "created": "2026-10-28T10:16:12.000Z",
  "livemode": true,
  "data": {
    "job": {
      "id": "3f6c2c0e-6d1b-4f1c-9a2f-1d7e2b5a9c10",
      "tool": "compress",
      "state": "done",
      "error": null,
      "outputs": [
        {
          "name": "report.pdf",
          "size": 1102337,
          "contentType": "application/pdf"
        }
      ]
    }
  }
}

livemode is false for a job made with a test key. Bodies carry no download links: read the job with your key for those.

The signature

Every delivery carries these headers:

Headers
NameTypeDescription
UVPDF-Signaturet=1761646572,v1=5f1c…An HMAC-SHA256 of "<t>.<body>" under your secret, hex; t is the time in seconds.
UVPDF-Eventjob.succeededThe event type, as in the body.
UVPDF-Delivery8c1d2f30-…The delivery’s id. The same event can be sent twice; skip an id you have seen.
Content-Typeapplication/json

The signature is an HMAC-SHA256, in hex, of the timestamp, a dot and the raw body, under your secret: HMAC_SHA256(secret, "<t>.<body>"). Check it on the bytes as they arrived, before parsing anything, and refuse a timestamp more than a few minutes old; a replayed body then fails on its age.

Checking it

Verify a delivery
import { createHmac, timingSafeEqual } from 'node:crypto';

/** True when the body came from UV pdfTool, unchanged, within the last five minutes. */
export function verifyWebhook(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(signatureHeader.split(',').map((part) => part.split('=')));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const given = String(parts.v1 ?? '');
  // Hex only: then both are the same length in bytes, as timingSafeEqual needs.
  return /^[0-9a-f]{64}$/.test(given) && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}

// In an Express handler, keep the raw body: express.raw({ type: 'application/json' })
app.post('/webhooks/uvpdftool', express.raw({ type: 'application/json' }), (request, response) => {
  const body = request.body.toString('utf8');
  if (!verifyWebhook(body, request.get('UVPDF-Signature') ?? '', process.env.UVPDF_WEBHOOK_SECRET)) {
    return response.status(400).end();
  }
  const event = JSON.parse(body);
  if (event.type === 'job.succeeded') queueDownload(event.data.job.id);
  response.status(200).end();
});
Frameworks that parse JSON before your code runs give you an object, not the bytes; re-serialising it will not match the signature. Read the raw body (the samples show how for Express, Flask and plain PHP).

Retries and the log

  • Any 2xx answer counts as received. Answer quickly and do the work afterwards.
  • Otherwise we try again after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h, then give up on that delivery.
  • We follow no redirects and wait 10 seconds at most; the address must resolve to a public host.
  • The same event can arrive twice, for example when your answer was lost: skip a UVPDF-Delivery id you have already handled.
  • The keys page lists the last deliveries with their status and any error, keeps them 30 days, and says since when an address has been failing.