Claude Code Plugins

Community-maintained marketplace

Feedback

webhook-integration

@aj-geddes/useful-ai-prompts
4
0

Implement secure webhook systems for event-driven integrations, including signature verification, retry logic, and delivery guarantees. Use when building third-party integrations, event notifications, or real-time data synchronization.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name webhook-integration
description Implement secure webhook systems for event-driven integrations, including signature verification, retry logic, and delivery guarantees. Use when building third-party integrations, event notifications, or real-time data synchronization.

Webhook Integration

Overview

Implement robust webhook systems for event-driven architectures, enabling real-time communication between services and third-party integrations.

When to Use

  • Third-party service integrations (Stripe, GitHub, Shopify)
  • Event notification systems
  • Real-time data synchronization
  • Automated workflow triggers
  • Payment processing callbacks
  • CI/CD pipeline notifications
  • User activity tracking
  • Microservices communication

Webhook Architecture

┌──────────┐         ┌──────────┐         ┌──────────┐
│  Event   │────────▶│ Webhook  │────────▶│ Consumer │
│  Source  │         │  Sender  │         │ Endpoint │
└──────────┘         └──────────┘         └──────────┘
                           │
                           ▼
                    ┌──────────────┐
                    │ Retry Queue  │
                    │ (Failed)     │
                    └──────────────┘

Implementation Examples

1. Webhook Sender (TypeScript)

import crypto from 'crypto';
import axios from 'axios';

interface WebhookEvent {
  id: string;
  type: string;
  timestamp: number;
  data: any;
}

interface WebhookEndpoint {
  url: string;
  secret: string;
  events: string[];
  active: boolean;
}

interface DeliveryAttempt {
  attemptNumber: number;
  timestamp: number;
  statusCode?: number;
  error?: string;
  duration: number;
}

class WebhookSender {
  private maxRetries = 3;
  private retryDelays = [1000, 5000, 30000]; // Exponential backoff
  private timeout = 10000; // 10 seconds

  /**
   * Generate HMAC signature for webhook payload
   */
  private generateSignature(
    payload: string,
    secret: string
  ): string {
    return crypto
      .createHmac('sha256', secret)
      .update(payload)
      .digest('hex');
  }

  /**
   * Send webhook to endpoint
   */
  async send(
    endpoint: WebhookEndpoint,
    event: WebhookEvent
  ): Promise<DeliveryAttempt[]> {
    if (!endpoint.active) {
      throw new Error('Endpoint is not active');
    }

    if (!endpoint.events.includes(event.type)) {
      throw new Error(`Event type ${event.type} not subscribed`);
    }

    const payload = JSON.stringify(event);
    const signature = this.generateSignature(payload, endpoint.secret);

    const attempts: DeliveryAttempt[] = [];

    for (let attempt = 0; attempt < this.maxRetries; attempt++) {
      const startTime = Date.now();

      try {
        const response = await axios.post(endpoint.url, payload, {
          headers: {
            'Content-Type': 'application/json',
            'X-Webhook-Signature': signature,
            'X-Webhook-ID': event.id,
            'X-Webhook-Timestamp': event.timestamp.toString(),
            'User-Agent': 'WebhookService/1.0'
          },
          timeout: this.timeout,
          validateStatus: (status) => status >= 200 && status < 300
        });

        const duration = Date.now() - startTime;

        attempts.push({
          attemptNumber: attempt + 1,
          timestamp: Date.now(),
          statusCode: response.status,
          duration
        });

        console.log(
          `Webhook delivered successfully to ${endpoint.url} (attempt ${attempt + 1})`
        );

        return attempts;
      } catch (error: any) {
        const duration = Date.now() - startTime;

        attempts.push({
          attemptNumber: attempt + 1,
          timestamp: Date.now(),
          statusCode: error.response?.status,
          error: error.message,
          duration
        });

        console.error(
          `Webhook delivery failed to ${endpoint.url} (attempt ${attempt + 1}):`,
          error.message
        );

        // Wait before retry (except on last attempt)
        if (attempt < this.maxRetries - 1) {
          await this.delay(this.retryDelays[attempt]);
        }
      }
    }

    throw new Error(
      `Webhook delivery failed after ${this.maxRetries} attempts`
    );
  }

  /**
   * Batch send webhooks
   */
  async sendBatch(
    endpoints: WebhookEndpoint[],
    event: WebhookEvent
  ): Promise<Map<string, DeliveryAttempt[]>> {
    const results = new Map<string, DeliveryAttempt[]>();

    await Promise.allSettled(
      endpoints.map(async (endpoint) => {
        try {
          const attempts = await this.send(endpoint, event);
          results.set(endpoint.url, attempts);
        } catch (error) {
          console.error(`Failed to deliver to ${endpoint.url}:`, error);
        }
      })
    );

    return results;
  }

  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// Usage
const sender = new WebhookSender();

const endpoint: WebhookEndpoint = {
  url: 'https://api.example.com/webhooks',
  secret: 'your-webhook-secret',
  events: ['user.created', 'user.updated'],
  active: true
};

const event: WebhookEvent = {
  id: crypto.randomUUID(),
  type: 'user.created',
  timestamp: Date.now(),
  data: {
    userId: '123',
    email: 'user@example.com'
  }
};

await sender.send(endpoint, event);

2. Webhook Receiver (Express)

import express from 'express';
import crypto from 'crypto';
import { body, validationResult } from 'express-validator';

interface WebhookConfig {
  secret: string;
  signatureHeader: string;
  timestampTolerance: number; // seconds
}

class WebhookReceiver {
  constructor(private config: WebhookConfig) {}

  /**
   * Verify webhook signature
   */
  verifySignature(
    payload: string,
    signature: string
  ): boolean {
    const expectedSignature = crypto
      .createHmac('sha256', this.config.secret)
      .update(payload)
      .digest('hex');

    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignature)
    );
  }

  /**
   * Verify timestamp to prevent replay attacks
   */
  verifyTimestamp(timestamp: number): boolean {
    const now = Date.now();
    const diff = Math.abs(now - timestamp) / 1000;

    return diff <= this.config.timestampTolerance;
  }

  /**
   * Middleware for webhook verification
   */
  createMiddleware() {
    return async (
      req: express.Request,
      res: express.Response,
      next: express.NextFunction
    ) => {
      try {
        const signature = req.headers[this.config.signatureHeader] as string;
        const timestamp = parseInt(
          req.headers['x-webhook-timestamp'] as string
        );

        if (!signature) {
          return res.status(401).json({
            error: 'Missing signature'
          });
        }

        // Verify timestamp
        if (!this.verifyTimestamp(timestamp)) {
          return res.status(401).json({
            error: 'Invalid timestamp'
          });
        }

        // Get raw body for signature verification
        const payload = JSON.stringify(req.body);

        // Verify signature
        if (!this.verifySignature(payload, signature)) {
          return res.status(401).json({
            error: 'Invalid signature'
          });
        }

        next();
      } catch (error) {
        console.error('Webhook verification error:', error);
        res.status(500).json({
          error: 'Verification failed'
        });
      }
    };
  }
}

// Setup Express app
const app = express();

// Use raw body parser for signature verification
app.use(express.json({
  verify: (req: any, res, buf) => {
    req.rawBody = buf.toString();
  }
}));

const receiver = new WebhookReceiver({
  secret: process.env.WEBHOOK_SECRET!,
  signatureHeader: 'x-webhook-signature',
  timestampTolerance: 300 // 5 minutes
});

// Webhook endpoint
app.post('/webhooks',
  receiver.createMiddleware(),
  [
    body('id').isString(),
    body('type').isString(),
    body('data').isObject()
  ],
  async (req, res) => {
    // Validate request
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }

    const { id, type, data } = req.body;

    try {
      // Process webhook event
      await processWebhookEvent(type, data);

      // Respond immediately
      res.status(200).json({
        received: true,
        eventId: id
      });

      // Process asynchronously if needed
      processEventAsync(type, data).catch(console.error);
    } catch (error) {
      console.error('Webhook processing error:', error);
      res.status(500).json({
        error: 'Processing failed'
      });
    }
  }
);

async function processWebhookEvent(type: string, data: any): Promise<void> {
  switch (type) {
    case 'user.created':
      await handleUserCreated(data);
      break;
    case 'payment.success':
      await handlePaymentSuccess(data);
      break;
    default:
      console.log(`Unknown event type: ${type}`);
  }
}

async function processEventAsync(type: string, data: any): Promise<void> {
  // Heavy processing that doesn't need to block the response
}

async function handleUserCreated(data: any): Promise<void> {
  console.log('User created:', data);
}

async function handlePaymentSuccess(data: any): Promise<void> {
  console.log('Payment successful:', data);
}

app.listen(3000, () => {
  console.log('Webhook receiver listening on port 3000');
});

3. Webhook Queue with Bull

import Queue from 'bull';
import axios from 'axios';

interface WebhookJob {
  endpoint: WebhookEndpoint;
  event: WebhookEvent;
}

class WebhookQueue {
  private queue: Queue.Queue<WebhookJob>;

  constructor(redisUrl: string) {
    this.queue = new Queue('webhooks', redisUrl, {
      defaultJobOptions: {
        attempts: 3,
        backoff: {
          type: 'exponential',
          delay: 2000
        },
        removeOnComplete: 100,
        removeOnFail: 1000
      }
    });

    this.setupProcessors();
    this.setupEventHandlers();
  }

  private setupProcessors(): void {
    // Process webhook deliveries
    this.queue.process('delivery', 5, async (job) => {
      const { endpoint, event } = job.data;

      job.log(`Delivering webhook to ${endpoint.url}`);

      const sender = new WebhookSender();
      const attempts = await sender.send(endpoint, event);

      return {
        endpoint: endpoint.url,
        attempts,
        success: true
      };
    });
  }

  private setupEventHandlers(): void {
    this.queue.on('completed', (job, result) => {
      console.log(`Webhook delivered: ${job.id}`, result);
    });

    this.queue.on('failed', (job, err) => {
      console.error(`Webhook delivery failed: ${job?.id}`, err);
    });

    this.queue.on('stalled', (job) => {
      console.warn(`Webhook delivery stalled: ${job.id}`);
    });
  }

  async enqueue(
    endpoint: WebhookEndpoint,
    event: WebhookEvent,
    options?: Queue.JobOptions
  ): Promise<Queue.Job<WebhookJob>> {
    return this.queue.add(
      'delivery',
      { endpoint, event },
      {
        jobId: `${event.id}-${endpoint.url}`,
        ...options
      }
    );
  }

  async enqueueBatch(
    endpoints: WebhookEndpoint[],
    event: WebhookEvent
  ): Promise<Queue.Job<WebhookJob>[]> {
    const jobs = endpoints.map(endpoint => ({
      name: 'delivery',
      data: { endpoint, event },
      opts: {
        jobId: `${event.id}-${endpoint.url}`
      }
    }));

    return this.queue.addBulk(jobs);
  }

  async getJobStatus(jobId: string): Promise<any> {
    const job = await this.queue.getJob(jobId);
    if (!job) return null;

    return {
      id: job.id,
      state: await job.getState(),
      progress: job.progress(),
      attempts: job.attemptsMade,
      failedReason: job.failedReason,
      finishedOn: job.finishedOn,
      processedOn: job.processedOn
    };
  }

  async retryFailed(jobId: string): Promise<void> {
    const job = await this.queue.getJob(jobId);
    if (!job) {
      throw new Error('Job not found');
    }

    await job.retry();
  }

  async pause(): Promise<void> {
    await this.queue.pause();
  }

  async resume(): Promise<void> {
    await this.queue.resume();
  }

  async close(): Promise<void> {
    await this.queue.close();
  }
}

// Usage
const webhookQueue = new WebhookQueue('redis://localhost:6379');

// Enqueue single webhook
await webhookQueue.enqueue(endpoint, event, {
  delay: 1000, // Delay 1 second
  priority: 1
});

// Enqueue to multiple endpoints
await webhookQueue.enqueueBatch(endpoints, event);

// Check job status
const status = await webhookQueue.getJobStatus('job-id');
console.log('Job status:', status);

4. Webhook Testing Utilities

import express from 'express';
import crypto from 'crypto';

class WebhookTester {
  private app: express.Application;
  private receivedEvents: WebhookEvent[] = [];

  constructor() {
    this.app = express();
    this.setupTestEndpoint();
  }

  private setupTestEndpoint(): void {
    this.app.use(express.json());

    this.app.post('/test-webhook', (req, res) => {
      const event = req.body;

      // Validate signature if provided
      const signature = req.headers['x-webhook-signature'] as string;
      if (signature) {
        // Verify signature here
      }

      // Store received event
      this.receivedEvents.push(event);

      console.log('Received webhook:', event);

      // Respond based on test scenario
      res.status(200).json({
        received: true,
        eventId: event.id
      });
    });

    // Endpoint that simulates failures
    this.app.post('/test-webhook/fail', (req, res) => {
      const failureType = req.query.type;

      switch (failureType) {
        case 'timeout':
          // Don't respond (simulates timeout)
          break;
        case 'server-error':
          res.status(500).json({ error: 'Internal server error' });
          break;
        case 'unauthorized':
          res.status(401).json({ error: 'Unauthorized' });
          break;
        default:
          res.status(400).json({ error: 'Bad request' });
      }
    });
  }

  start(port: number): void {
    this.app.listen(port, () => {
      console.log(`Webhook test server running on port ${port}`);
    });
  }

  getReceivedEvents(): WebhookEvent[] {
    return this.receivedEvents;
  }

  clearEvents(): void {
    this.receivedEvents = [];
  }

  /**
   * Create mock webhook event
   */
  static createMockEvent(type: string, data: any): WebhookEvent {
    return {
      id: crypto.randomUUID(),
      type,
      timestamp: Date.now(),
      data
    };
  }
}

// Testing
const tester = new WebhookTester();
tester.start(3001);

// Send test webhook
const mockEvent = WebhookTester.createMockEvent('user.created', {
  userId: '123',
  email: 'test@example.com'
});

const sender = new WebhookSender();
await sender.send(
  {
    url: 'http://localhost:3001/test-webhook',
    secret: 'test-secret',
    events: ['user.created'],
    active: true
  },
  mockEvent
);

// Verify received
const received = tester.getReceivedEvents();
console.log('Received events:', received);

Best Practices

✅ DO

  • Use HMAC signatures for verification
  • Implement idempotency with event IDs
  • Return 200 OK quickly, process asynchronously
  • Implement exponential backoff for retries
  • Include timestamp to prevent replay attacks
  • Use queue systems for reliable delivery
  • Log all delivery attempts
  • Provide webhook testing tools
  • Document webhook payload schemas
  • Implement webhook management UI
  • Allow filtering by event types
  • Support webhook versioning

❌ DON'T

  • Send sensitive data in webhooks
  • Skip signature verification
  • Block responses with heavy processing
  • Retry indefinitely
  • Expose internal error details
  • Send webhooks to localhost (in production)
  • Forget timeout handling
  • Skip rate limiting

Security Checklist

  • Verify signatures using HMAC
  • Check timestamp to prevent replay attacks
  • Validate SSL certificates
  • Use HTTPS only
  • Implement rate limiting
  • Validate webhook URLs
  • Rotate secrets periodically
  • Log security events
  • Implement IP whitelisting (optional)
  • Sanitize error messages

Resources