Webhooks
Get notified when renders complete instead of polling. Webhooks are HTTP POST requests sent to your server when specific events occur.
Note: Webhook delivery system is currently in development. The interface documented here reflects the planned implementation. For now, use polling (GET /api/render/:id) to check render status.
Why Webhooks?
Webhooks are more efficient than polling:
- No need to poll every 5 seconds — we notify you when the video is ready
- Reduces API calls and rate limit consumption
- Lower latency — get notified within seconds of completion
- Supports batch render completion events
Webhooks are ideal for asynchronous workflows where you can't maintain a long-running polling loop (e.g., serverless functions, background jobs, CI/CD pipelines).
Setting Up Webhooks
1. Create a Webhook Endpoint
Your webhook endpoint should:
- Accept POST requests
- Return a
200 OKresponse within 5 seconds - Verify the webhook signature (see Verification)
- Process events asynchronously (queue them, don't block the response)
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
app.post('/webhooks/sigfigs', (req, res) => {
const signature = req.headers['x-sigfigs-signature'];
const secret = process.env.SIGFIGS_WEBHOOK_SECRET;
// Verify signature
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(req.body))
.digest('hex');
if (signature !== expectedSignature) {
return res.status(401).send('Invalid signature');
}
// Queue event for async processing
eventQueue.add(req.body);
// Respond immediately
res.status(200).send('OK');
});
app.listen(3000);from flask import Flask, request, jsonify
import hmac
import hashlib
import os
app = Flask(__name__)
@app.route('/webhooks/sigfigs', methods=['POST'])
def webhook():
signature = request.headers.get('X-Sigfigs-Signature')
secret = os.environ['SIGFIGS_WEBHOOK_SECRET']
# Verify signature
body = request.get_data()
expected_signature = hmac.new(
secret.encode(),
body,
hashlib.sha256
).hexdigest()
if signature != expected_signature:
return jsonify({'error': 'Invalid signature'}), 401
# Queue event for async processing
event_queue.enqueue(request.json)
# Respond immediately
return jsonify({'status': 'ok'}), 200
if __name__ == '__main__':
app.run(port=3000)2. Register Your Webhook URL
Register your endpoint via the dashboard or API:
curl -X POST https://app.sigfigsstudio.com/api/webhooks \
-H "Content-Type: application/json" \
-H "X-API-Key: sf_live_YOUR_KEY" \
-d '{
"url": "https://yourdomain.com/webhooks/sigfigs",
"events": ["render.complete", "render.failed", "batch.complete"]
}'{
"success": true,
"webhook": {
"id": "wh_abc123",
"url": "https://yourdomain.com/webhooks/sigfigs",
"events": ["render.complete", "render.failed", "batch.complete"],
"secret": "whsec_XyZ789AbCdEf...",
"active": true
},
"message": "Save the secret — it's used to verify webhook signatures."
}Tip: You can register multiple webhook URLs for different events or different environments (staging, production).
Event Types
Significant Figures sends webhooks for these events:
| Event | Description | Typical Delay |
|---|---|---|
render.complete | Single render finished successfully | 30-120s |
render.failed | Single render failed | 10-60s |
batch.complete | All jobs in a batch finished (may include failures) | Varies |
batch.progress | Batch milestone reached (25%, 50%, 75%) | Varies |
Payload Format
All webhook payloads follow this structure:
{
"event": "render.complete",
"timestamp": "2026-03-19T19:19:20.614Z",
"data": {
"jobId": "78008b5b-4207-4b2e-a1c5-9b923f519d4c",
"templateId": "emeritus",
"status": "complete",
"outputUrl": "https://skkroo2x4wm2v7c8.public.blob.vercel-storage.com/emeritus/78008b5b.mp4",
"displayName": "Dr. Jane Smith",
"createdAt": "2026-03-19T19:18:53.849Z",
"completedAt": "2026-03-19T19:19:20.614Z",
"renderDurationMs": 26765
}
}{
"event": "render.failed",
"timestamp": "2026-03-19T19:19:12.345Z",
"data": {
"jobId": "abc123-...",
"templateId": "laureate",
"status": "failed",
"error": "External data source unavailable: Hardcover API timeout",
"createdAt": "2026-03-19T19:18:45.123Z",
"failedAt": "2026-03-19T19:19:12.345Z"
}
}{
"event": "batch.complete",
"timestamp": "2026-03-19T20:15:30.123Z",
"data": {
"batchId": "batch-xyz789",
"templateId": "emeritus",
"totalJobs": 100,
"completed": 98,
"failed": 2,
"createdAt": "2026-03-19T19:45:00.000Z",
"completedAt": "2026-03-19T20:15:30.123Z",
"durationMs": 1830123,
"jobIds": ["job-1", "job-2", "..."]
}
}Signature Verification
Every webhook includes an X-Sigfigs-Signature header. Verify this signature to ensure the request came from Significant Figures and wasn't tampered with.
How It Works
- We compute HMAC-SHA256 of the request body using your webhook secret
- The resulting hex digest is sent as
X-Sigfigs-Signature - Your server computes the same HMAC using the same secret
- If the signatures match, the webhook is authentic
Node.js Example
const crypto = require('crypto');
function verifyWebhook(body, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(body))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Usage
const isValid = verifyWebhook(
req.body,
req.headers['x-sigfigs-signature'],
process.env.SIGFIGS_WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}Python Example
import hmac
import hashlib
def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
expected_signature = hmac.new(
secret.encode(),
body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
# Usage
body = request.get_data()
signature = request.headers.get('X-Sigfigs-Signature')
secret = os.environ['SIGFIGS_WEBHOOK_SECRET']
if not verify_webhook(body, signature, secret):
return jsonify({'error': 'Invalid signature'}), 401Security: Always verify signatures before processing webhook payloads. Without verification, an attacker could send fake events to your endpoint.
Retry Policy
If your endpoint doesn't respond with 200 OK, we'll retry delivery with exponential backoff:
| Attempt | Delay | Timeout |
|---|---|---|
| 1 (initial) | — | 5s |
| 2 | 1 min | 5s |
| 3 | 5 min | 5s |
| 4 | 30 min | 10s |
| 5 | 2 hours | 10s |
| 6 (final) | 6 hours | 10s |
After 6 failed attempts (over ~8.5 hours), we'll mark the webhook delivery as failed and stop retrying. You can view failed deliveries in your dashboard.
Idempotency
Due to retries, your endpoint may receive the same event multiple times. Use the jobId or batchId to deduplicate events.
const processedEvents = new Set();
app.post('/webhooks/sigfigs', async (req, res) => {
const { event, data } = req.body;
const eventKey = `${event}:${data.jobId || data.batchId}`;
// Check if already processed
if (processedEvents.has(eventKey)) {
return res.status(200).send('Already processed');
}
// Mark as processed
processedEvents.add(eventKey);
// Process event
await handleEvent(req.body);
res.status(200).send('OK');
});Testing Webhooks
Local Development
Use a tool like ngrok orlocaltunnel to expose your local server to the internet:
# Start your local webhook server node server.js # Listening on http://localhost:3000 # In another terminal, start ngrok ngrok http 3000 # Copy the ngrok URL (e.g., https://abc123.ngrok.io) # Register it as your webhook URL in Significant Figures
Trigger Test Event
Send a test event to verify your endpoint and signature verification:
curl -X POST https://app.sigfigsstudio.com/api/webhooks/test \
-H "X-API-Key: sf_live_YOUR_KEY" \
-d '{"webhookId": "wh_abc123"}'This sends a test.event with a sample payload to your registered URL.
Best Practices
Respond Quickly
Return 200 OK within 5 seconds. Queue events for async processing instead of doing heavy work in the request handler.
Always Verify Signatures
Don't skip signature verification, even in development. It's your only guarantee the webhook is authentic.
Handle Retries Idempotently
Use jobId/batchId to deduplicate events. Retries can happen if your server is slow or temporarily down.
Monitor Webhook Failures
Check your dashboard regularly for failed deliveries. Set up alerts if your endpoint is consistently failing.
Use HTTPS
Webhook URLs must use HTTPS in production. We won't send webhooks to HTTP endpoints.
Webhooks coming soon
In the meantime, use polling to check render status. Check back for webhook availability updates.