DocsWebhooks

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 OK response within 5 seconds
  • Verify the webhook signature (see Verification)
  • Process events asynchronously (queue them, don't block the response)
Node.js (Express)
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);
Python (Flask)
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
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"]
  }'
Response
{
  "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:

EventDescriptionTypical Delay
render.completeSingle render finished successfully30-120s
render.failedSingle render failed10-60s
batch.completeAll jobs in a batch finished (may include failures)Varies
batch.progressBatch milestone reached (25%, 50%, 75%)Varies

Payload Format

All webhook payloads follow this structure:

render.complete event
{
  "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
  }
}
render.failed event
{
  "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"
  }
}
batch.complete event
{
  "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

  1. We compute HMAC-SHA256 of the request body using your webhook secret
  2. The resulting hex digest is sent as X-Sigfigs-Signature
  3. Your server computes the same HMAC using the same secret
  4. If the signatures match, the webhook is authentic

Node.js Example

verify.js
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

verify.py
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'}), 401

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

AttemptDelayTimeout
1 (initial)5s
21 min5s
35 min5s
430 min10s
52 hours10s
6 (final)6 hours10s

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.

Idempotent handler (Node.js)
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:

Terminal
# 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
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.