Skip to main content
When you set webhookUrl (and webhookSecret) on POST .../generate-async, PDF Gorilla sends an HTTP POST to your URL when the job reaches a final state: completed, failed, or canceled. Each delivery is signed so you can prove it came from PDF Gorilla and that the body was not changed.

Setup

curl -X POST https://pdfgorilla.io/api/v1/templates/TEMPLATE_ID/generate-async \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "data": { "orderId": "ORD-999" },
    "webhookUrl": "https://your-server.com/webhooks/pdf",
    "webhookSecret": "at-least-32-random-bytes-please"
  }'
webhookSecret is required whenever webhookUrl is set. The API rejects the request if it is missing. Max length is 512 characters.

Rules for webhookUrl

  • Must use https://
  • Must be reachable from the public internet (no localhost)
  • Your handler should return 2xx within about 5 seconds

Pick a strong secret

Generate one:
openssl rand -hex 32
Use a different secret per URL so a leak in one integration does not expose others.

Payload

Headers

HeaderMeaning
Content-Typeapplication/json
x-pdfg-event-idSame as jobId (dedupe deliveries)
x-pdfg-signatureHMAC-SHA256 of the raw body, hex encoded

Completed job

{
  "jobId": "cma1b2c3d4e5f6g7h8i9j0k",
  "status": "completed",
  "downloadUrl": "https://...",
  "expiresAt": "2026-04-13T10:00:00.000Z",
  "error": null,
  "timestamp": "2026-04-06T10:01:23.456Z"
}

Failed job

{
  "jobId": "cma1b2c3d4e5f6g7h8i9j0k",
  "status": "failed",
  "error": {
    "code": "RENDER_TIMEOUT",
    "message": "PDF rendering timed out."
  },
  "timestamp": "2026-04-06T10:01:23.456Z"
}
FieldNotes
downloadUrlOnly on completed. Short lifetime; fetch job status again if it expires.
expiresAtPresent with downloadUrl on completed.
error{ "code": "...", "message": "..." } on failure, else null.

Verify the signature

Compute:
hex( HMAC-SHA256( webhookSecret, rawBodyBytes ) )
Compare to x-pdfg-signature with a timing-safe compare. Use the raw request body string before parsing JSON. Re-serializing JSON breaks the signature.

Node.js

import { createHmac, timingSafeEqual } from "node:crypto";

function verifyWebhookSignature(
  rawBody: string,
  signature: string,
  secret: string
): boolean {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  if (expected.length !== signature.length) return false;
  return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Python

import hmac
import hashlib

def verify(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)

Security checklist

Use timing-safe comparison, not plain === on strings.
Verify the raw body, not a re-encoded copy of parsed JSON.
Use a unique secret per webhook endpoint.
Reply with 2xx quickly; slow handlers may time out.
Deduplicate using x-pdfg-event-id / jobId in your handler.
Only treat money-moving actions as done after you validate status and signature.

Download URL lifetime

Links in the webhook expire after a few minutes. If you need the file later, call GET /api/v1/jobs/{jobId} for a new URL. Stored files follow the retention rules in Limits.