Skip to main content

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:
  1. Accept POST requests
  2. Verify the signature
  3. Return 200 OK quickly (process async if needed)
  4. 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:
AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
624 hours
After 10 consecutive failures, the webhook is auto-disabled. Re-enable via PATCH once the issue is resolved.

Best Practices

  1. Return quickly: Process events asynchronously and return 200 immediately
  2. Handle duplicates: Use the event id for idempotency
  3. Verify signatures: Always verify the signature before processing
  4. Log everything: Log event IDs for debugging
  5. 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);