home / skills / jeremylongshore / claude-code-plugins-plus-skills / retellai-webhooks-events

This skill enables secure Retell AI webhook integration by validating signatures, preventing replays, and routing events to handlers.

npx playbooks add skill jeremylongshore/claude-code-plugins-plus-skills --skill retellai-webhooks-events

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
5.4 KB
---
name: retellai-webhooks-events
description: |
  Implement Retell AI webhook signature validation and event handling.
  Use when setting up webhook endpoints, implementing signature verification,
  or handling Retell AI event notifications securely.
  Trigger with phrases like "retellai webhook", "retellai events",
  "retellai webhook signature", "handle retellai events", "retellai notifications".
allowed-tools: Read, Write, Edit, Bash(curl:*)
version: 1.0.0
license: MIT
author: Jeremy Longshore <[email protected]>
---

# Retell AI Webhooks & Events

## Overview
Securely handle Retell AI webhooks with signature validation and replay protection.

## Prerequisites
- Retell AI webhook secret configured
- HTTPS endpoint accessible from internet
- Understanding of cryptographic signatures
- Redis or database for idempotency (optional)

## Webhook Endpoint Setup

### Express.js
```typescript
import express from 'express';
import crypto from 'crypto';

const app = express();

// IMPORTANT: Raw body needed for signature verification
app.post('/webhooks/retellai',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const signature = req.headers['x-retellai-signature'] as string;
    const timestamp = req.headers['x-retellai-timestamp'] as string;

    if (!verifyRetell AISignature(req.body, signature, timestamp)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    const event = JSON.parse(req.body.toString());
    await handleRetell AIEvent(event);

    res.status(200).json({ received: true });
  }
);
```

## Signature Verification

```typescript
function verifyRetell AISignature(
  payload: Buffer,
  signature: string,
  timestamp: string
): boolean {
  const secret = process.env.RETELLAI_WEBHOOK_SECRET!;

  // Reject old timestamps (replay attack protection)
  const timestampAge = Date.now() - parseInt(timestamp) * 1000;
  if (timestampAge > 300000) { // 5 minutes
    console.error('Webhook timestamp too old');
    return false;
  }

  // Compute expected signature
  const signedPayload = `${timestamp}.${payload.toString()}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // Timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}
```

## Event Handler Pattern

```typescript
type Retell AIEventType = 'resource.created' | 'resource.updated' | 'resource.deleted';

interface Retell AIEvent {
  id: string;
  type: Retell AIEventType;
  data: Record<string, any>;
  created: string;
}

const eventHandlers: Record<Retell AIEventType, (data: any) => Promise<void>> = {
  'resource.created': async (data) => { /* handle */ },
  'resource.updated': async (data) => { /* handle */ },
  'resource.deleted': async (data) => { /* handle */ }
};

async function handleRetell AIEvent(event: Retell AIEvent): Promise<void> {
  const handler = eventHandlers[event.type];

  if (!handler) {
    console.log(`Unhandled event type: ${event.type}`);
    return;
  }

  try {
    await handler(event.data);
    console.log(`Processed ${event.type}: ${event.id}`);
  } catch (error) {
    console.error(`Failed to process ${event.type}: ${event.id}`, error);
    throw error; // Rethrow to trigger retry
  }
}
```

## Idempotency Handling

```typescript
import { Redis } from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

async function isEventProcessed(eventId: string): Promise<boolean> {
  const key = `retellai:event:${eventId}`;
  const exists = await redis.exists(key);
  return exists === 1;
}

async function markEventProcessed(eventId: string): Promise<void> {
  const key = `retellai:event:${eventId}`;
  await redis.set(key, '1', 'EX', 86400 * 7); // 7 days TTL
}
```

## Webhook Testing

```bash
# Use Retell AI CLI to send test events
retellai webhooks trigger resource.created --url http://localhost:3000/webhooks/retellai

# Or use webhook.site for debugging
curl -X POST https://webhook.site/your-uuid \
  -H "Content-Type: application/json" \
  -d '{"type": "resource.created", "data": {}}'
```

## Instructions

### Step 1: Register Webhook Endpoint
Configure your webhook URL in the Retell AI dashboard.

### Step 2: Implement Signature Verification
Use the signature verification code to validate incoming webhooks.

### Step 3: Handle Events
Implement handlers for each event type your application needs.

### Step 4: Add Idempotency
Prevent duplicate processing with event ID tracking.

## Output
- Secure webhook endpoint
- Signature validation enabled
- Event handlers implemented
- Replay attack protection active

## Error Handling
| Issue | Cause | Solution |
|-------|-------|----------|
| Invalid signature | Wrong secret | Verify webhook secret |
| Timestamp rejected | Clock drift | Check server time sync |
| Duplicate events | Missing idempotency | Implement event ID tracking |
| Handler timeout | Slow processing | Use async queue |

## Examples

### Testing Webhooks Locally
```bash
# Use ngrok to expose local server
ngrok http 3000

# Send test webhook
curl -X POST https://your-ngrok-url/webhooks/retellai \
  -H "Content-Type: application/json" \
  -d '{"type": "test", "data": {}}'
```

## Resources
- [Retell AI Webhooks Guide](https://docs.retellai.com/webhooks)
- [Webhook Security Best Practices](https://docs.retellai.com/webhooks/security)

## Next Steps
For performance optimization, see `retellai-performance-tuning`.

Overview

This skill implements Retell AI webhook signature validation, replay protection, and event handling patterns for secure webhook endpoints. It provides a tested verification flow, event dispatching, and optional idempotency helpers to prevent duplicate processing. Use it to harden integrations that receive Retell AI notifications.

How this skill works

Incoming requests must include Retell AI headers (signature and timestamp). The skill verifies signatures by computing an HMAC-SHA256 over the timestamp + raw body and performing a timing-safe comparison. It rejects stale timestamps to prevent replay attacks. Events are parsed and dispatched to typed handlers; processed event IDs can be recorded in Redis to guarantee idempotency.

When to use it

  • Setting up a new Retell AI webhook endpoint
  • Implementing signature verification for incoming Retell AI notifications
  • Protecting endpoints from replay attacks and tampering
  • Ensuring idempotent processing of events in distributed systems
  • Testing and debugging webhook delivery locally or in staging

Best practices

  • Require raw request body for signature verification (do not parse JSON before verifying)
  • Reject timestamps older than a short window (e.g., 5 minutes) to limit replay attacks
  • Use timing-safe comparisons when comparing signatures to avoid leaking timing information
  • Record processed event IDs (Redis or database) with a TTL to achieve idempotency
  • Respond quickly (200) after enqueueing work; handle long processing asynchronously via a job queue
  • Monitor webhook failures and surface non-200 responses to the dashboard

Example use cases

  • Receiving resource.created/resource.updated/resource.deleted events and updating local models
  • Triggering asynchronous workflows (jobs, notifications) when Retell AI emits events
  • Protecting a public webhook endpoint against forged requests using HMAC signatures
  • Debugging webhook delivery with CLI or webhook.site while validating signature logic
  • Maintaining eventual consistency by deduplicating retries with Redis-backed event tracking

FAQ

What headers are required for verification?

Include x-retellai-signature and x-retellai-timestamp; compute HMAC-SHA256 with your webhook secret over timestamp + raw body.

How do I prevent duplicate processing?

Store each event ID (e.g., retellai:event:<id>) in Redis or a DB with a TTL and skip processing if the ID already exists.