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.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS URL to receive webhook events |
events | array | No | Event types to subscribe to (default: all) |
secret | string | No | Custom secret for signature verification |
Event Types
| Event | Description |
|---|---|
pdf.generated | PDF was successfully generated |
pdf.failed | PDF generation failed |
pdf.stored | PDF was stored successfully |
file.deleted | Stored 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.
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.
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
| Header | Description |
|---|---|
Content-Type | application/json |
X-Webhook-ID | Unique ID for this webhook delivery |
X-Webhook-Signature | HMAC-SHA256 signature of the payload |
X-Webhook-Timestamp | Unix 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:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 6 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