SyntaxdocSyntaxdoc

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

  1. Configure: Set a webhook URL in your batch request or API key settings
  2. Generate: Start batch PDF generation with stream: true
  3. Receive: Get HTTP POST notification when batch completes or fails
  4. 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
  }
}
FieldTypeDescription
eventstringEvent type: batch.completed
batchIdstringUnique batch identifier
timestampstringISO 8601 timestamp
data.documentsGeneratednumberNumber of PDFs created
data.downloadUrlstringURL to download ZIP file
data.zipIdstringHosted ZIP file ID
data.expiresAtstringExpiration 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-uuid

Use 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 3000

Use 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:

  1. Visit webhook.site and copy your unique URL
  2. Use that URL in your webhook configuration
  3. Generate a batch and view the webhook payload

Troubleshooting

Webhook Not Received

Check these common issues:

  1. Endpoint not publicly accessible

    • Ensure your server is reachable from the internet
    • Use ngrok for local development
  2. Firewall blocking requests

    • Whitelist SyntaxDoc IP ranges (contact support)
    • Check cloud provider security groups
  3. Wrong HTTP method

    • Webhooks use POST, not GET
    • Ensure your route accepts POST requests
  4. 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

FeatureWebhooksSSEPolling
Real-time✅ Yes✅ Yes⚠️ Delayed
ConnectionNone neededPersistentMultiple requests
Server loadMinimalModerateHigh
ReliabilityRetry logicConnection dropsMost reliable
Best forProduction appsReal-time UIsSimple 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

On this page