Agent Skills: Webhook Integration

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.

UncategorizedID: aj-geddes/useful-ai-prompts/webhook-integration

Install this agent skill to your local

pnpm dlx add-skill https://github.com/aj-geddes/useful-ai-prompts/tree/HEAD/skills/webhook-integration

Skill Files

Browse the full folder contents for webhook-integration.

Download Skill

Loading file tree…

skills/webhook-integration/SKILL.md

Skill Metadata

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