Skip to main content

Webhook Subscriptions

Receive real-time HTTP notifications when document generation events occur. Webhooks eliminate the need to poll for status updates.

Overview

When you create a webhook subscription, RenderDoc will send HTTP POST requests to your specified URL whenever matching events occur.

Supported Events

EventDescription
document.generatedDocument successfully generated
document.failedDocument generation failed
batch.completedBatch processing completed
batch.failedBatch processing failed

Getting Started

Create a Webhook Endpoint

Create an HTTPS endpoint on your server that accepts POST requests:

// Express.js example
app.post('/webhooks/renderdoc', express.json(), (req, res) => {
const event = req.body;

console.log('Received event:', event.event);
console.log('Job ID:', event.data.jobId);

// Process the event
switch (event.event) {
case 'document.generated':
// Document is ready - save download URL or notify user
console.log('Download URL:', event.data.downloadUrl);
break;
case 'document.failed':
// Handle failure - retry or alert
console.log('Error:', event.data.error);
break;
case 'batch.completed':
// Process batch results
console.log('Documents:', event.data.completedDocuments);
break;
}

// Always respond with 200 OK quickly
res.status(200).send('OK');
});
warning

Your endpoint must respond with 2xx status within 30 seconds, or the delivery will be marked as failed and retried.

Create a Webhook Subscription

Use the API or dashboard to create a subscription:

const response = await fetch('https://api.renderdoc.dev/api/v1/webhook-subscriptions', {
method: 'POST',
headers: {
'Authorization': 'Bearer your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: 'https://yourapp.com/webhooks/renderdoc',
events: ['document.generated', 'document.failed', 'batch.completed'],
description: 'Production webhook for document events'
})
});

const subscription = await response.json();
// Save subscription.secret - you'll need it to verify signatures
console.log('Secret:', subscription.secret);

Via Dashboard:

  1. Go to SettingsWebhooks
  2. Click Create Webhook
  3. Enter your endpoint URL
  4. Select the events you want to receive
  5. Click Create
  6. Copy the signing secret

Verify Webhook Signatures

Always verify webhook signatures to ensure requests are from RenderDoc:

const crypto = require('crypto');

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

return signature === `v1=${expectedSignature}`;
}

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

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

// Process verified webhook
// ...

res.status(200).send('OK');
});

Test Your Webhook

Send a test event to verify your endpoint:

await fetch(`https://api.renderdoc.dev/api/v1/webhook-subscriptions/${subscriptionId}/test`, {
method: 'POST',
headers: {
'Authorization': 'Bearer your-api-key',
'Content-Type': 'application/json'
}
});

Webhook Payload Format

Every webhook delivery includes these headers and a JSON body:

Headers

HeaderDescriptionExample
X-RenderDoc-SignatureHMAC-SHA256 signaturev1=abc123...
X-RenderDoc-TimestampUnix timestamp1732723200
X-RenderDoc-Event-IdUnique event identifierevt_xxx
X-RenderDoc-Event-TypeEvent typedocument.generated
Content-TypeAlways JSONapplication/json

Body Structure

{
"event": "document.generated",
"timestamp": "2024-11-27T12:00:00.000Z",
"data": {
"jobId": "550e8400-e29b-41d4-a716-446655440000",
"templateId": "invoice-template",
"format": "pdf",
"status": "completed",
"downloadUrl": "https://storage.renderdoc.dev/...",
"expiresAt": "2024-11-30T12:00:00.000Z",
"metadata": {
"orderId": "12345"
}
}
}

Event Payloads

document.generated

{
"event": "document.generated",
"timestamp": "2024-11-27T12:00:00.000Z",
"data": {
"jobId": "uuid",
"templateId": "invoice-template",
"format": "pdf",
"status": "completed",
"downloadUrl": "https://storage.renderdoc.dev/...",
"expiresAt": "2024-11-30T12:00:00.000Z",
"generatedAt": "2024-11-27T12:00:00.000Z",
"metadata": {}
}
}

document.failed

{
"event": "document.failed",
"timestamp": "2024-11-27T12:00:10.000Z",
"data": {
"jobId": "uuid",
"templateId": "invoice-template",
"format": "pdf",
"status": "failed",
"error": "Missing required variable: customerName",
"errorCode": "MISSING_VARIABLE",
"failedAt": "2024-11-27T12:00:10.000Z",
"metadata": {}
}
}

batch.completed

{
"event": "batch.completed",
"timestamp": "2024-11-27T12:05:00.000Z",
"data": {
"batchId": "uuid",
"templateId": "invoice-template",
"format": "pdf",
"status": "completed",
"totalDocuments": 100,
"completedDocuments": 98,
"failedDocuments": 2,
"completedAt": "2024-11-27T12:05:00.000Z",
"documents": [
{
"jobId": "uuid1",
"status": "completed",
"downloadUrl": "https://..."
},
{
"jobId": "uuid2",
"status": "failed",
"error": "Missing variable"
}
]
}
}

batch.failed

{
"event": "batch.failed",
"timestamp": "2024-11-27T12:05:00.000Z",
"data": {
"batchId": "uuid",
"templateId": "invoice-template",
"format": "pdf",
"status": "failed",
"error": "Template not found",
"errorCode": "TEMPLATE_NOT_FOUND",
"failedAt": "2024-11-27T12:05:00.000Z"
}
}

API Reference

List Subscriptions

GET /api/v1/webhook-subscriptions

curl https://api.renderdoc.dev/api/v1/webhook-subscriptions \
-H "Authorization: Bearer your-api-key"

Create Subscription

POST /api/v1/webhook-subscriptions

{
"url": "https://yourapp.com/webhooks/renderdoc",
"events": ["document.generated", "document.failed"],
"description": "Production webhook",
"enabled": true
}

Response includes the signing secret:

{
"id": "wh_xxxxx",
"url": "https://yourapp.com/webhooks/renderdoc",
"events": ["document.generated", "document.failed"],
"secret": "whsec_xxxxxxxxxxxxx",
"enabled": true,
"createdAt": "2024-11-27T12:00:00.000Z"
}

Get Subscription

GET /api/v1/webhook-subscriptions/:id

Update Subscription

PATCH /api/v1/webhook-subscriptions/:id

{
"events": ["document.generated", "document.failed", "batch.completed"],
"enabled": true
}

Delete Subscription

DELETE /api/v1/webhook-subscriptions/:id

Send Test Webhook

POST /api/v1/webhook-subscriptions/:id/test

Sends a test document.generated event to your endpoint.

Get Delivery History

GET /api/v1/webhook-subscriptions/:id/deliveries

View recent delivery attempts:

{
"deliveries": [
{
"id": "del_xxxxx",
"event": "document.generated",
"status": "success",
"statusCode": 200,
"responseTime": 150,
"attemptedAt": "2024-11-27T12:00:00.000Z"
},
{
"id": "del_yyyyy",
"event": "document.failed",
"status": "failed",
"statusCode": 500,
"error": "Internal Server Error",
"attemptedAt": "2024-11-27T11:55:00.000Z",
"nextRetryAt": "2024-11-27T11:56:00.000Z"
}
]
}

Retry Failed Delivery

POST /api/v1/webhook-subscriptions/:id/deliveries/:deliveryId/retry

Manually retry a failed delivery.

Signature Verification

Algorithm

RenderDoc uses HMAC-SHA256 for webhook signatures:

  1. Construct the signed payload: {timestamp}.{json_body}
  2. Compute HMAC-SHA256 with your webhook secret
  3. Compare with the signature header (after removing v1= prefix)

Code Examples

Node.js:

const crypto = require('crypto');

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

return `v1=${expectedSig}` === signature;
}

Python:

import hmac
import hashlib
import json

def verify_signature(payload, signature, timestamp, secret):
signed_payload = f"{timestamp}.{json.dumps(payload, separators=(',', ':'))}"
expected_sig = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
return f"v1={expected_sig}" == signature

Java:

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.HexFormat;

public boolean verifySignature(String payload, String signature, String timestamp, String secret) {
String signedPayload = timestamp + "." + payload;
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
String expectedSig = "v1=" + HexFormat.of().formatHex(hmac.doFinal(signedPayload.getBytes()));
return expectedSig.equals(signature);
}

Timestamp Validation

Prevent replay attacks by validating the timestamp:

function isTimestampValid(timestamp, toleranceSeconds = 300) {
const now = Math.floor(Date.now() / 1000);
const eventTime = parseInt(timestamp, 10);
return Math.abs(now - eventTime) <= toleranceSeconds;
}

Retry Policy

Failed deliveries are automatically retried with exponential backoff:

AttemptDelay
1Immediate
21 minute
35 minutes
4 (final)30 minutes

After 4 failed attempts, the delivery is marked as failed. You can manually retry from the delivery history.

What Counts as Failure

  • Non-2xx response status
  • Connection timeout (30 seconds)
  • Connection refused
  • SSL/TLS errors

Best Practices

Respond Quickly

Return 200 OK as fast as possible. Process events asynchronously:

app.post('/webhooks/renderdoc', express.json(), async (req, res) => {
// Verify signature first
if (!verifySignature(req.body, req.headers['x-renderdoc-signature'], req.headers['x-renderdoc-timestamp'], secret)) {
return res.status(401).send('Invalid signature');
}

// Acknowledge receipt immediately
res.status(200).send('OK');

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

async function processWebhookAsync(event) {
switch (event.event) {
case 'document.generated':
await saveDocumentUrl(event.data.jobId, event.data.downloadUrl);
break;
case 'document.failed':
await alertOnFailure(event.data);
break;
case 'batch.completed':
await processBatchResults(event.data);
break;
}
}

Handle Duplicates

Webhooks may be delivered more than once. Use the X-RenderDoc-Event-Id header for idempotency:

const processedEvents = new Set(); // Use Redis in production

app.post('/webhooks/renderdoc', async (req, res) => {
const eventId = req.headers['x-renderdoc-event-id'];

if (processedEvents.has(eventId)) {
return res.status(200).send('Already processed');
}

// Process event
// ...

processedEvents.add(eventId);
res.status(200).send('OK');
});

Use HTTPS

Webhook URLs must use HTTPS. Self-signed certificates are not supported.

Rotate Secrets

If you suspect your webhook secret is compromised, create a new subscription and delete the old one.

Troubleshooting

Not Receiving Webhooks

  1. Check subscription is enabled: View subscription in dashboard
  2. Verify URL is accessible: Can RenderDoc reach your endpoint?
  3. Check firewall rules: Allow incoming connections
  4. Review delivery history: Check for failed deliveries

Signature Verification Failing

  1. Use raw body: Don't parse JSON before verification
  2. Check secret: Make sure you're using the correct secret
  3. Check timestamp: Ensure your server clock is accurate

Slow Processing

  1. Respond quickly: Return 200 before processing
  2. Use queue: Process events asynchronously
  3. Batch updates: Don't hit your database for every event