> ## Documentation Index
> Fetch the complete documentation index at: https://docs.unleeshed.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhook Integration Guide

> Set up real-time notifications for commentary approval events

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

```bash theme={null}
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"]
  }'
```

<Warning>
  Save the `secret` from the response - it's only shown once and is required for signature verification.
</Warning>

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

```typescript theme={null}
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

```typescript theme={null}
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:

```bash theme={null}
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

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:

```typescript theme={null}
const processedEvents = new Set();

if (processedEvents.has(event.id)) {
  return; // Already processed
}
processedEvents.add(event.id);
```
