Skip to content

Webhooks

Receive completion events with webhooks.

Runs long operations, such as workflows on large files or batches of up to 50 PDFs, as background jobs on the PDF API, and sends a signed webhook to an address you choose when each one finishes. Events never include document contents.

How to use it

  1. Start the API with a webhook secret. Your receiver uses the same secret to check that events come from superPDF.
  2. Send POST /v1/jobs with the tool name, its input and a callback_url.
  3. When the job finishes, superPDF sends a signed event to that address. Check the signature, answer with a 2xx status, and fetch the job if you need its full result.

Start the API with a webhook secret

Terminal
export SUPERPDF_API_TOKEN="$(openssl rand -hex 32)"
# The secret superPDF signs events with; your receiver checks it
export SUPERPDF_WEBHOOK_SECRET="whsec_$(openssl rand -base64 32)"
superpdf serve --allow-dir ~/Documents/superPDF

Without SUPERPDF_WEBHOOK_SECRET, the API makes a new secret each time it starts and prints it. Export the same value in the Terminal window where your receiver runs.

Start a job

Add callback_url to the job. The TypeScript, Python and Go examples use the client file.

curl http://127.0.0.1:7341/v1/jobs \
  -H "Authorization: Bearer $SUPERPDF_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tool": "pdf_run_batch",
    "input": {
      "inputs": ["report.pdf", "appendix.pdf"],
      "steps": [{ "action": "watermark", "options": { "text": "DRAFT" } }],
      "output_folder": "drafts"
    },
    "callback_url": "http://127.0.0.1:8080/hooks/superpdf"
  }'

Events

When the job finishes, superPDF sends one POST to callback_url, signed in the Standard Webhooks format:

Request
POST /hooks/superpdf HTTP/1.1
content-type: application/json
user-agent: superPDF/0.1.0
webhook-id: msg_5dfdfc12-4548-4899-bfb9-9c37addfc594
webhook-timestamp: 1791682743
webhook-signature: v1,wp2AKnccq3DgITSFsK4PkiEjt8f2lEjJF1uNfEBYX9c=

{
  "type": "job.succeeded",
  "timestamp": "2026-10-11T01:39:03.906Z",
  "data": {
    "job_id": "job_f5a4c3fc-c43f-4655-8a35-f8821bf7cd4f",
    "tool": "pdf_run_batch",
    "status": "succeeded",
    "created_at": "2026-10-11T01:39:03.888Z",
    "finished_at": "2026-10-11T01:39:03.906Z",
    "outputs": [
      "/Users/<you>/Documents/superPDF/drafts/report-processed.pdf",
      "/Users/<you>/Documents/superPDF/drafts/appendix-processed.pdf"
    ]
  }
}

Verify events

These receivers check the signature and the timestamp, skip repeats, answer at once and print each event:

receiver.ts
import { createServer, type IncomingHttpHeaders } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';

interface JobEvent {
  type: 'job.succeeded' | 'job.failed';
  timestamp: string;
  data: {
    job_id: string;
    tool: string;
    status: 'succeeded' | 'failed';
    outputs: string[];
    error?: { code: string; detail: string };
  };
}

const secret = process.env.SUPERPDF_WEBHOOK_SECRET ?? '';
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const handled = new Set<string>();

/** Standard Webhooks: "v1," + base64 HMAC-SHA256 of "id.timestamp.body". */
function verify(headers: IncomingHttpHeaders, body: Buffer): boolean {
  const id = String(headers['webhook-id'] ?? '');
  const timestamp = String(headers['webhook-timestamp'] ?? '');
  if (!id || !/^\d+$/.test(timestamp)) return false;
  // Refuse events more than five minutes old (or ahead)
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const mac = createHmac('sha256', key).update(`${id}.${timestamp}.`).update(body);
  const expected = Buffer.from(`v1,${mac.digest('base64')}`);
  const signatures = String(headers['webhook-signature'] ?? '').split(' ');
  return signatures.some(signature => {
    const given = Buffer.from(signature);
    return given.length === expected.length && timingSafeEqual(given, expected);
  });
}

const server = createServer(async (req, res) => {
  if (req.method !== 'POST' || req.url !== '/hooks/superpdf') {
    res.writeHead(404).end();
    return;
  }
  const chunks: Buffer[] = [];
  for await (const chunk of req) chunks.push(chunk);
  const body = Buffer.concat(chunks);
  if (!verify(req.headers, body)) {
    res.writeHead(401).end();
    return;
  }
  res.writeHead(204).end(); // answer at once: superPDF waits 10 seconds at most

  const id = String(req.headers['webhook-id']);
  if (handled.has(id)) return; // a retry of an event already handled
  handled.add(id);
  const event: JobEvent = JSON.parse(body.toString('utf8'));
  const files = event.data.outputs.length;
  console.log(`${event.type} ${event.data.job_id}: ${files} file(s)`);
});
server.listen(8080, '127.0.0.1', () => {
  console.log('Listening on http://127.0.0.1:8080/hooks/superpdf');
});

Jobs and webhooks explains each step, replay protection, retries and the addresses events may go to.

Privacy

Runs on your computer. The superpdf command-line tool reads and writes files only in the folders you allow, and uploads nothing.