Webhook Integration
This guide walks you through setting up webhooks to receive real-time notifications for commentary approval events.
Overview
Webhooks provide push-based delivery so you don’t need to poll the API. When a creator or partner approves commentary, we’ll send an HTTP POST to your endpoint with the full commentary content.
Step 1: Register Your Webhook
First, register your webhook endpoint:
curl -X POST https://prod.api.unleeshed.ai/partner/v1/webhooks \
-H "X-Api-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-domain.com/webhooks/unleeshed",
"events": ["creator.approved", "partner.approved", "commentary.failed"]
}'
Save the secret from the response - it’s only shown once and is required for signature verification.
Step 2: Implement Your Endpoint
Your endpoint should:
- Accept POST requests
- Verify the signature
- Return 200 OK quickly (process async if needed)
- Handle duplicate events idempotently (use the
id field)
Signature Verification
All webhook payloads are signed with HMAC-SHA256. Verify the signature to ensure the request is from Unleeshed.
import crypto from 'crypto';
function verifyWebhookSignature(
signatureHeader: string,
payload: string,
secret: string,
toleranceSeconds = 300
): boolean {
// Parse header: t=timestamp,v1=signature
const parts = Object.fromEntries(
signatureHeader.split(',').map(part => part.split('='))
);
const timestamp = parseInt(parts.t, 10);
const now = Math.floor(Date.now() / 1000);
// Reject if timestamp is too old (replay protection)
if (Math.abs(now - timestamp) > toleranceSeconds) {
return false;
}
// Compute expected signature
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${payload}`)
.digest('hex');
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(parts.v1),
Buffer.from(expected)
);
}
Example Express Handler
import express from 'express';
const app = express();
const WEBHOOK_SECRET = process.env.UNLEESHED_WEBHOOK_SECRET;
app.post('/webhooks/unleeshed', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-unleeshed-signature'] as string;
const payload = req.body.toString();
if (!verifyWebhookSignature(signature, payload, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
// Idempotency: Check if we've already processed this event
if (await hasProcessedEvent(event.id)) {
return res.status(200).send('Already processed');
}
// Process event asynchronously
processEventAsync(event).catch(console.error);
// Return 200 immediately
res.status(200).send('OK');
});
async function processEventAsync(event) {
switch (event.type) {
case 'creator.approved':
// Creator approved — commentary is ready for partner review
const { external_reference: creatorRef, commentary, metadata } = event.data;
await flagForPartnerReview(creatorRef, commentary, metadata);
break;
case 'partner.approved':
// Partner approved — commentary is ready for distribution
const { external_reference: partnerRef, commentary: approvedCommentary } = event.data;
await publishContent(partnerRef, approvedCommentary);
break;
case 'commentary.failed':
// Commentary generation failed or was rejected
const { external_reference: failedRef, error } = event.data;
await handleFailure(failedRef, error);
break;
}
// Mark event as processed
await markEventProcessed(event.id);
}
Step 3: Test Your Webhook
Use the test endpoint to verify your setup:
curl -X POST https://prod.api.unleeshed.ai/partner/v1/webhooks/whk_abc123/test \
-H "X-Api-Key: your_api_key"
Check the response to confirm your endpoint is receiving and verifying webhooks correctly.
Retry Behavior
If your endpoint returns a non-2xx status code or times out, we’ll retry:
| Attempt | Delay |
|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 24 hours |
After 10 consecutive failures, the webhook is auto-disabled. Re-enable via PATCH once the issue is resolved.
Best Practices
- Return quickly: Process events asynchronously and return 200 immediately
- Handle duplicates: Use the event
id for idempotency
- Verify signatures: Always verify the signature before processing
- Log everything: Log event IDs for debugging
- Monitor failures: Set up alerts for consecutive failures
Troubleshooting
Webhook Not Receiving Events
- Check webhook status is
active
- Verify URL is accessible from the internet
- Check firewall/security rules allow Unleeshed IPs
Signature Verification Failing
- Ensure you’re using the correct secret (from registration)
- Verify the raw body is used (not parsed JSON)
- Check timestamp tolerance (default 5 minutes)
Duplicate Events
This is expected behavior. Use the id field for deduplication:
const processedEvents = new Set();
if (processedEvents.has(event.id)) {
return; // Already processed
}
processedEvents.add(event.id);