Webhooks
Get async notifications when batch PDF generation completes
Webhooks provide real-time notifications when your batch PDF generation completes or fails, eliminating the need for polling or maintaining SSE connections.
Enterprise-Grade Async Processing - Perfect for large batches and background jobs. No need to keep connections open!
How Webhooks Work
- Configure: Set a webhook URL in your batch request or API key settings
- Generate: Start batch PDF generation with
stream: true - Receive: Get HTTP POST notification when batch completes or fails
- Process: Handle the webhook payload in your application
Setup
Option 1: Per-Request Webhook
Include webhook configuration in your batch generation request:
const response = await fetch('https://api.syntaxdoc.com/pdf/generate-batch', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
documents: [...],
stream: true,
host: true,
webhook: {
url: 'https://your-server.com/webhooks/syntaxdoc'
}
})
});Option 2: Default Webhook (Coming Soon)
Configure a default webhook URL for your API key via the dashboard. This webhook will be used for all batch requests unless overridden.
Webhook Payload
Success Event: batch.completed
Sent when batch generation completes successfully:
{
"event": "batch.completed",
"batchId": "abc123-batch-uuid",
"timestamp": "2024-12-06T10:30:00.000Z",
"data": {
"documentsGenerated": 50,
"downloadUrl": "https://api.syntaxdoc.com/api/hosted-pdfs/zip-abc123",
"zipId": "zip-abc123",
"expiresAt": "2024-12-13T10:30:00.000Z",
"total": 50
}
}| Field | Type | Description |
|---|---|---|
event | string | Event type: batch.completed |
batchId | string | Unique batch identifier |
timestamp | string | ISO 8601 timestamp |
data.documentsGenerated | number | Number of PDFs created |
data.downloadUrl | string | URL to download ZIP file |
data.zipId | string | Hosted ZIP file ID |
data.expiresAt | string | Expiration timestamp |
Failure Event: batch.failed
Sent when batch generation fails:
{
"event": "batch.failed",
"batchId": "abc123-batch-uuid",
"timestamp": "2024-12-06T10:30:00.000Z",
"data": {
"error": "Insufficient conversions"
}
}HTTP Headers
All webhook requests include these headers:
Content-Type: application/json
User-Agent: SyntaxDoc-Webhook/1.0
X-Webhook-Event: batch.completed
X-Batch-Id: abc123-batch-uuidUse these headers to verify the webhook origin and parse the event type.
Implementing Your Endpoint
Express.js Example
import express from 'express';
const app = express();
app.use(express.json());
app.post('/webhooks/syntaxdoc', (req, res) => {
const { event, batchId, data } = req.body;
console.log(`Received webhook: ${event} for batch ${batchId}`);
if (event === 'batch.completed') {
console.log(`✅ Batch completed: ${data.documentsGenerated} PDFs`);
console.log(`Download: ${data.downloadUrl}`);
// Process the completed batch
// - Send emails with download links
// - Update database records
// - Trigger next workflow step
} else if (event === 'batch.failed') {
console.error(`❌ Batch failed: ${data.error}`);
// Handle failure (retry, notify admin, etc.)
}
// IMPORTANT: Respond with 2xx to acknowledge receipt
res.status(200).json({ received: true });
});
app.listen(3000);// app/api/webhooks/syntaxdoc/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const payload = await request.json();
const { event, batchId, data } = payload;
console.log(`Webhook: ${event} - Batch ${batchId}`);
if (event === 'batch.completed') {
// Process completed batch
await processCompletedBatch({
batchId,
documentsGenerated: data.documentsGenerated,
downloadUrl: data.downloadUrl,
zipId: data.zipId,
});
} else if (event === 'batch.failed') {
// Handle failure
await handleBatchFailure(batchId, data.error);
}
return NextResponse.json({ received: true });
}
async function processCompletedBatch(batch: any) {
// Your business logic here
console.log(`Processing ${batch.documentsGenerated} PDFs`);
}
async function handleBatchFailure(batchId: string, error: string) {
console.error(`Batch ${batchId} failed:`, error);
}from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/syntaxdoc', methods=['POST'])
def handle_webhook():
payload = request.get_json()
event = payload.get('event')
batch_id = payload.get('batchId')
data = payload.get('data')
print(f"Received webhook: {event} for batch {batch_id}")
if event == 'batch.completed':
print(f"✅ Batch completed: {data['documentsGenerated']} PDFs")
print(f"Download: {data['downloadUrl']}")
# Process completed batch
elif event == 'batch.failed':
print(f"❌ Batch failed: {data['error']}")
# Handle failure
# Return 200 to acknowledge receipt
return jsonify({'received': True}), 200
if __name__ == '__main__':
app.run(port=3000)Your webhook endpoint must respond with HTTP 200-299 within 10 seconds to be considered successful.
Retry Logic
If your endpoint fails to respond or returns a non-2xx status, SyntaxDoc automatically retries:
- Attempt 1: Immediate
- Attempt 2: After 2 seconds
- Attempt 3: After 4 seconds
- Attempt 4: After 8 seconds
After 3 failed attempts, the webhook is abandoned. Check your server logs and ensure your endpoint is publicly accessible.
Security Best Practices
1. Verify Webhook Origin
Check the User-Agent and custom headers:
app.post('/webhooks/syntaxdoc', (req, res) => {
const userAgent = req.headers['user-agent'];
const webhookEvent = req.headers['x-webhook-event'];
if (!userAgent?.includes('SyntaxDoc-Webhook')) {
return res.status(403).json({ error: 'Invalid webhook source' });
}
// Process webhook
});2. Use HTTPS
Always use HTTPS URLs for webhooks to prevent man-in-the-middle attacks:
webhook: {
url: 'https://your-server.com/webhooks/syntaxdoc' // ✅ HTTPS
// url: 'http://your-server.com/webhooks' // ❌ Not allowed
}3. Validate Batch ID
Store batch IDs when you create them, then verify incoming webhooks:
const activeBatches = new Set();
// When creating batch
const { batchId } = await createBatch(...);
activeBatches.add(batchId);
// In webhook handler
app.post('/webhooks/syntaxdoc', (req, res) => {
const { batchId } = req.body;
if (!activeBatches.has(batchId)) {
return res.status(400).json({ error: 'Unknown batch ID' });
}
activeBatches.delete(batchId);
// Process webhook
});4. Implement Idempotency
Handle duplicate webhook deliveries gracefully:
const processedWebhooks = new Set();
app.post('/webhooks/syntaxdoc', (req, res) => {
const { batchId, event } = req.body;
const webhookId = `${batchId}-${event}`;
if (processedWebhooks.has(webhookId)) {
console.log('Duplicate webhook, ignoring');
return res.status(200).json({ received: true });
}
processedWebhooks.add(webhookId);
// Process webhook
});Testing Webhooks
Local Testing with ngrok
Use ngrok to expose your local server:
# Start your local server
node server.js
# In another terminal, start ngrok
ngrok http 3000Use the ngrok HTTPS URL in your webhook configuration:
webhook: {
url: 'https://abc123.ngrok.io/webhooks/syntaxdoc'
}Manual Testing
Use webhook testing tools like webhook.site to inspect payloads:
- Visit webhook.site and copy your unique URL
- Use that URL in your webhook configuration
- Generate a batch and view the webhook payload
Troubleshooting
Webhook Not Received
Check these common issues:
-
Endpoint not publicly accessible
- Ensure your server is reachable from the internet
- Use ngrok for local development
-
Firewall blocking requests
- Whitelist SyntaxDoc IP ranges (contact support)
- Check cloud provider security groups
-
Wrong HTTP method
- Webhooks use POST, not GET
- Ensure your route accepts POST requests
-
Timeout
- Respond within 10 seconds
- Process heavy work asynchronously
Debugging
Add detailed logging to your webhook handler:
app.post('/webhooks/syntaxdoc', (req, res) => {
console.log('=== WEBHOOK RECEIVED ===');
console.log('Headers:', req.headers);
console.log('Body:', JSON.stringify(req.body, null, 2));
console.log('========================');
// Your logic here
res.status(200).json({ received: true });
});Comparison: Webhooks vs SSE vs Polling
| Feature | Webhooks | SSE | Polling |
|---|---|---|---|
| Real-time | ✅ Yes | ✅ Yes | ⚠️ Delayed |
| Connection | None needed | Persistent | Multiple requests |
| Server load | Minimal | Moderate | High |
| Reliability | Retry logic | Connection drops | Most reliable |
| Best for | Production apps | Real-time UIs | Simple setups |
Recommendation: Use webhooks for production, SSE for live dashboards, polling for simple clients or testing.
Example: Full Workflow
Here's a complete example of generating 100 certificates with webhook notification:
// 1. Start batch generation
const response = await fetch('https://api.syntaxdoc.com/pdf/generate-batch', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
documents: Array.from({ length: 100 }, (_, i) => ({
fileName: `certificate-${i + 1}`,
htmlContent: `<html><body><h1>Certificate ${i + 1}</h1></body></html>`
})),
batchName: 'employee-certificates',
stream: true,
host: true,
webhook: {
url: 'https://your-server.com/webhooks/syntaxdoc'
}
})
});
const { batchId } = await response.json();
console.log(`Batch started: ${batchId}`);
// 2. Your webhook endpoint receives completion
app.post('/webhooks/syntaxdoc', async (req, res) => {
const { event, batchId, data } = req.body;
if (event === 'batch.completed') {
console.log(`✅ ${data.documentsGenerated} certificates ready!`);
// Download the ZIP
const zipResponse = await fetch(data.downloadUrl);
const zipBuffer = await zipResponse.arrayBuffer();
// Extract and email individual PDFs
await sendCertificatesToEmployees(zipBuffer);
// Update database
await db.updateBatchStatus(batchId, 'completed');
}
res.status(200).json({ received: true });
});Rate Limits
Webhook deliveries are not rate-limited, but your endpoint should handle high volumes if you generate many batches concurrently.
Recommendations:
- Use a queue (Redis, Bull, etc.) to process webhooks asynchronously
- Scale your webhook endpoint horizontally
- Monitor response times and optimize slow operations