Webhooks

Configure webhooks to receive notifications about PDF generation events.

Webhooks

Configure webhooks to receive real-time notifications when PDF generation events occur. Webhooks enable asynchronous workflows and integration with external systems.


Configure Webhook

Set up or update your webhook configuration.

POST /v1/webhooks/config

Request Body

ParameterTypeRequiredDescription
urlstringYesHTTPS URL to receive webhook events
eventsarrayNoEvent types to subscribe to (default: all)
secretstringNoCustom secret for signature verification

Event Types

EventDescription
pdf.generatedPDF was successfully generated
pdf.failedPDF generation failed
pdf.storedPDF was stored successfully
file.deletedStored file was deleted

Code Examples

curl -X POST https://api.pdfapi.dev/v1/webhooks/config \
  -H "Authorization: Bearer sk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/pdfapi",
    "events": ["pdf.generated", "pdf.failed"],
    "secret": "your-webhook-secret-key"
  }'
const response = await fetch('https://api.pdfapi.dev/v1/webhooks/config', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.PDFAPI_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    url: 'https://your-app.com/webhooks/pdfapi',
    events: ['pdf.generated', 'pdf.failed'],
    secret: 'your-webhook-secret-key'
  }),
});

const { data } = await response.json();
console.log('Webhook configured:', data.id);
import requests
import os

response = requests.post(
    'https://api.pdfapi.dev/v1/webhooks/config',
    headers={
        'Authorization': f'Bearer {os.environ["PDFAPI_KEY"]}',
        'Content-Type': 'application/json',
    },
    json={
        'url': 'https://your-app.com/webhooks/pdfapi',
        'events': ['pdf.generated', 'pdf.failed'],
        'secret': 'your-webhook-secret-key'
    }
)

config = response.json()['data']
print(f"Webhook ID: {config['id']}")
print(f"URL: {config['url']}")

Response

{
  "data": {
    "id": "wh_1234567890",
    "url": "https://your-app.com/webhooks/pdfapi",
    "events": ["pdf.generated", "pdf.failed"],
    "secret": "your-webhook-secret-key",
    "active": true,
    "created_at": "2025-01-20T10:30:00Z"
  }
}

Get Webhook Configuration

Retrieve your current webhook configuration.

GET /v1/webhooks/config

Code Examples

curl https://api.pdfapi.dev/v1/webhooks/config \
  -H "Authorization: Bearer sk_live_xxx"
const response = await fetch('https://api.pdfapi.dev/v1/webhooks/config', {
  headers: {
    'Authorization': `Bearer ${process.env.PDFAPI_KEY}`,
  },
});

const { data } = await response.json();
if (data) {
  console.log('Webhook URL:', data.url);
  console.log('Events:', data.events);
} else {
  console.log('No webhook configured');
}
import requests
import os

response = requests.get(
    'https://api.pdfapi.dev/v1/webhooks/config',
    headers={'Authorization': f'Bearer {os.environ["PDFAPI_KEY"]}'}
)

config = response.json().get('data')
if config:
    print(f"Webhook URL: {config['url']}")
    print(f"Active: {config['active']}")

Response

{
  "data": {
    "id": "wh_1234567890",
    "url": "https://your-app.com/webhooks/pdfapi",
    "events": ["pdf.generated", "pdf.failed"],
    "active": true,
    "created_at": "2025-01-20T10:30:00Z",
    "last_triggered_at": "2025-01-22T14:30:00Z"
  }
}

Delete Webhook Configuration

Remove your webhook configuration.

DELETE /v1/webhooks/config

Code Examples

curl -X DELETE https://api.pdfapi.dev/v1/webhooks/config \
  -H "Authorization: Bearer sk_live_xxx"
const response = await fetch('https://api.pdfapi.dev/v1/webhooks/config', {
  method: 'DELETE',
  headers: {
    'Authorization': `Bearer ${process.env.PDFAPI_KEY}`,
  },
});

if (response.status === 204) {
  console.log('Webhook configuration deleted');
}
import requests
import os

response = requests.delete(
    'https://api.pdfapi.dev/v1/webhooks/config',
    headers={'Authorization': f'Bearer {os.environ["PDFAPI_KEY"]}'}
)

if response.status_code == 204:
    print('Webhook configuration deleted')

Response

Returns 204 No Content on success.


Webhook Payload

When an event occurs, PDF API sends a POST request to your configured URL.

Headers

HeaderDescription
Content-Typeapplication/json
X-Webhook-IDUnique ID for this webhook delivery
X-Webhook-SignatureHMAC-SHA256 signature of the payload
X-Webhook-TimestampUnix timestamp of the event

Payload Structure

{
  "id": "evt_1234567890",
  "type": "pdf.generated",
  "timestamp": "2025-01-22T14:30:00Z",
  "data": {
    "request_id": "req_abcdef123456",
    "source": "markdown",
    "file_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "filename": "report.pdf",
    "size": 125430,
    "page_count": 5,
    "generation_time_ms": 1234
  }
}

Event-Specific Data

pdf.generated

{
  "type": "pdf.generated",
  "data": {
    "request_id": "req_abcdef123456",
    "source": "markdown",
    "file_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "filename": "report.pdf",
    "size": 125430,
    "page_count": 5,
    "generation_time_ms": 1234,
    "stored": true
  }
}

pdf.failed

{
  "type": "pdf.failed",
  "data": {
    "request_id": "req_abcdef123456",
    "source": "html",
    "error": {
      "code": "RENDER_TIMEOUT",
      "message": "Page rendering timed out after 30 seconds"
    }
  }
}

Verifying Webhook Signatures

Verify that webhook requests originate from PDF API by validating the signature.

Signature Verification

import crypto from 'crypto';

function verifyWebhookSignature(payload, signature, timestamp, secret) {
  const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// Express.js example
app.post('/webhooks/pdfapi', express.json(), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const timestamp = req.headers['x-webhook-timestamp'];

  if (!verifyWebhookSignature(req.body, signature, timestamp, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  // Process the webhook
  const { type, data } = req.body;
  console.log(`Received ${type} event:`, data);

  res.status(200).send('OK');
});
import hmac
import hashlib
import json
from flask import Flask, request, abort

app = Flask(__name__)

def verify_webhook_signature(payload, signature, timestamp, secret):
    signed_payload = f"{timestamp}.{json.dumps(payload, separators=(',', ':'))}"
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        signed_payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected_signature)

@app.route('/webhooks/pdfapi', methods=['POST'])
def webhook_handler():
    signature = request.headers.get('X-Webhook-Signature')
    timestamp = request.headers.get('X-Webhook-Timestamp')

    if not verify_webhook_signature(
        request.json, signature, timestamp, os.environ['WEBHOOK_SECRET']
    ):
        abort(401)

    event_type = request.json['type']
    event_data = request.json['data']

    print(f"Received {event_type} event:", event_data)

    return 'OK', 200
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "net/http"
)

func verifyWebhookSignature(payload []byte, signature, timestamp, secret string) bool {
    signedPayload := fmt.Sprintf("%s.%s", timestamp, string(payload))
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(signedPayload))
    expectedSignature := hex.EncodeToString(mac.Sum(nil))

    return hmac.Equal([]byte(signature), []byte(expectedSignature))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    signature := r.Header.Get("X-Webhook-Signature")
    timestamp := r.Header.Get("X-Webhook-Timestamp")

    var payload map[string]interface{}
    json.NewDecoder(r.Body).Decode(&payload)

    payloadBytes, _ := json.Marshal(payload)

    if !verifyWebhookSignature(payloadBytes, signature, timestamp, os.Getenv("WEBHOOK_SECRET")) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }

    fmt.Printf("Received %s event\n", payload["type"])
    w.WriteHeader(http.StatusOK)
}

Retry Policy

PDF API automatically retries failed webhook deliveries with exponential backoff:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
66 hours

After 6 failed attempts, the webhook is marked as failed and no further retries are made.

Success Criteria

A webhook delivery is considered successful when:

  • Your endpoint returns an HTTP 2xx status code
  • The response is received within 30 seconds

Best Practices

Respond Quickly

Return a 200 response immediately and process the webhook asynchronously:

app.post('/webhooks/pdfapi', (req, res) => {
  // Respond immediately
  res.status(200).send('OK');

  // Process asynchronously
  processWebhook(req.body).catch(console.error);
});

async function processWebhook(event) {
  switch (event.type) {
    case 'pdf.generated':
      await handlePdfGenerated(event.data);
      break;
    case 'pdf.failed':
      await handlePdfFailed(event.data);
      break;
  }
}

Handle Duplicates

Webhook deliveries may be retried, so ensure your handler is idempotent:

const processedEvents = new Set();

async function processWebhook(event) {
  if (processedEvents.has(event.id)) {
    console.log('Duplicate event, skipping:', event.id);
    return;
  }

  processedEvents.add(event.id);
  // Process the event...
}

Log Everything

Keep detailed logs for debugging:

import logging

logger = logging.getLogger(__name__)

@app.route('/webhooks/pdfapi', methods=['POST'])
def webhook_handler():
    logger.info(f"Received webhook: {request.json['id']} - {request.json['type']}")

    try:
        process_webhook(request.json)
        logger.info(f"Successfully processed webhook: {request.json['id']}")
    except Exception as e:
        logger.error(f"Failed to process webhook: {request.json['id']} - {str(e)}")
        raise

    return 'OK', 200

Error Responses

Invalid URL (400)

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Webhook URL must be a valid HTTPS URL"
  }
}

No Configuration (404)

{
  "error": {
    "code": "WEBHOOK_NOT_CONFIGURED",
    "message": "No webhook configuration found"
  }
}

See Error Codes for complete reference.


Notes

  • Webhook URLs must use HTTPS
  • Each account can have one webhook configuration
  • Webhooks are delivered within seconds of the event
  • Failed webhook deliveries are logged and can be viewed in your dashboard
  • Consider using a service like ngrok for local development testing