Claude Code for Webhooks: Reliable Delivery, Signatures, and Event Systems — Claude Skills 360 Blog
Blog / Backend / Claude Code for Webhooks: Reliable Delivery, Signatures, and Event Systems
Backend

Claude Code for Webhooks: Reliable Delivery, Signatures, and Event Systems

Published: November 12, 2026
Read time: 8 min read
By: Claude Skills 360

Webhooks are HTTP callbacks — your service notifies others when events happen. Done wrong: lost events, duplicate processing, no way to verify authenticity, no replay. Done right: signed payloads, at-least-once delivery with exponential retry, idempotency keys, a delivery log for debugging, and an event catalog that documents what gets sent. Claude Code writes the webhook delivery engine, HMAC signature utilities, idempotent consumer patterns, and the fanout architecture that broadcasts one event to many subscribers reliably.

CLAUDE.md for Webhook Systems

## Webhook Stack
- Delivery: queue-backed (Celery/BullMQ) with exponential backoff retry
- Signatures: HMAC-SHA256 with Stripe-style timestamp prefix
- Idempotency: event_id in payload; consumers store processed IDs in Redis/DB
- Retry policy: 5 attempts at 1min, 5min, 1hr, 6hr, 24hr
- Timeout: 10s per delivery attempt
- Delivery log: persist every attempt (status, response code, duration)
- Event versioning: event_type includes version (order.created.v1)

Webhook Delivery Engine

// webhooks/delivery.ts
import { Queue } from 'bullmq';
import crypto from 'crypto';
import { setTimeout as delay } from 'timers/promises';

interface WebhookEvent {
  id: string;
  type: string;
  version: string;
  created_at: string;
  data: Record<string, unknown>;
}

interface Subscription {
  id: string;
  url: string;
  secret: string;
  events: string[];  // e.g. ['order.created.v1', 'order.status.updated.v1']
}

// Job queue for async delivery
const webhookQueue = new Queue('webhooks', {
  defaultJobOptions: {
    attempts: 5,
    backoff: {
      type: 'custom',
      delay: (attempt: number) => [60_000, 300_000, 3_600_000, 21_600_000, 86_400_000][attempt - 1],
    },
  },
});

// Dispatch: called after any state change
export async function dispatchEvent(event: WebhookEvent): Promise<void> {
  // Find all subscriptions that want this event type
  const subscriptions = await db.getSubscriptions({ eventType: event.type });
  
  // Enqueue one job per subscription (fan-out)
  await webhookQueue.addBulk(
    subscriptions.map(sub => ({
      name: 'deliver',
      data: { event, subscription: sub },
    }))
  );
}

// Worker: deliver one event to one endpoint
export async function deliverWebhook(
  event: WebhookEvent,
  subscription: Subscription,
): Promise<void> {
  const body = JSON.stringify(event);
  const signature = signPayload(body, subscription.secret);
  
  const startTime = Date.now();
  let statusCode: number;
  
  try {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 10_000);
    
    const response = await fetch(subscription.url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Webhook-ID': event.id,
        'X-Webhook-Timestamp': Math.floor(Date.now() / 1000).toString(),
        'X-Webhook-Signature': signature,
      },
      body,
      signal: controller.signal,
    });
    
    clearTimeout(timeoutId);
    statusCode = response.status;
    
    await db.logDeliveryAttempt({
      eventId: event.id,
      subscriptionId: subscription.id,
      statusCode,
      duration: Date.now() - startTime,
      success: response.ok,
    });
    
    if (!response.ok) {
      throw new Error(`HTTP ${statusCode}: endpoint returned error`);
    }
  } catch (err) {
    if (err.name === 'AbortError') {
      throw new Error('Webhook delivery timed out after 10s');
    }
    throw err;  // BullMQ will retry based on job config
  }
}

function signPayload(body: string, secret: string): string {
  const timestamp = Math.floor(Date.now() / 1000);
  const toSign = `${timestamp}.${body}`;
  const hmac = crypto.createHmac('sha256', secret).update(toSign).digest('hex');
  return `t=${timestamp},v1=${hmac}`;
}

Signature Verification (Consumer Side)

// webhooks/verify.ts — used by webhook consumers to verify incoming payloads
export function verifyWebhookSignature(
  body: string,
  signatureHeader: string,
  secret: string,
  toleranceSeconds = 300,  // Reject webhooks older than 5 minutes
): boolean {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map(part => part.split('='))
  );
  
  const timestamp = parseInt(parts['t'] ?? '0', 10);
  const signatures = Object.entries(parts)
    .filter(([k]) => k.startsWith('v'))
    .map(([, v]) => v);
  
  // Reject stale webhooks (prevents replay attacks)
  const age = Math.abs(Date.now() / 1000 - timestamp);
  if (age > toleranceSeconds) return false;
  
  const toSign = `${timestamp}.${body}`;
  const expected = crypto.createHmac('sha256', secret).update(toSign).digest('hex');
  
  return signatures.some(sig => crypto.timingSafeEqual(
    Buffer.from(sig, 'hex'),
    Buffer.from(expected, 'hex'),
  ));
}

// Express middleware
export function requireValidWebhook(secret: string) {
  return (req: Request, res: Response, next: NextFunction) => {
    const rawBody = req.rawBody;  // Must be configured before JSON parsing
    const signature = req.headers['x-webhook-signature'] as string;
    
    if (!signature || !verifyWebhookSignature(rawBody, signature, secret)) {
      return res.status(401).json({ error: 'Invalid webhook signature' });
    }
    
    next();
  };
}

Idempotent Consumer

// webhooks/consumer.ts — safe to receive duplicate events
export async function handleOrderCreated(event: WebhookEvent): Promise<void> {
  // Check if already processed (idempotency)
  const alreadyProcessed = await redis.set(
    `webhook:processed:${event.id}`,
    '1',
    'EX', 86400,  // Dedup window: 24 hours
    'NX',         // Only set if not exists
  );
  
  if (!alreadyProcessed) {
    console.log(`Webhook ${event.id} already processed, skipping`);
    return;
  }
  
  // Safe to process
  const order = event.data as Order;
  await fulfillmentService.processNewOrder(order);
}

// FastAPI receiver
@app.post("/webhooks/orders")
async def receive_order_webhook(
    request: Request,
    x_webhook_signature: str = Header(...),
):
    body = await request.body()
    
    if not verify_signature(body.decode(), x_webhook_signature, WEBHOOK_SECRET):
        raise HTTPException(status_code=401, detail="Invalid signature")
    
    event = json.loads(body)
    
    # Process based on event type
    match event["type"]:
        case "order.created.v1":
            await handle_order_created(event)
        case "order.status.updated.v1":
            await handle_order_status_updated(event)
        case _:
            pass  # Unknown event type — ignore gracefully
    
    return {"ok": True}

Webhook Testing

// Test: mock delivery and verify signatures
describe('webhook delivery', () => {
  it('sends correct HMAC signature', async () => {
    const mockServer = setupMockServer();
    const subscription = { url: mockServer.url, secret: 'test-secret', events: ['order.created.v1'] };
    
    await deliverWebhook(
      { id: 'evt-1', type: 'order.created.v1', version: '1', created_at: new Date().toISOString(), data: {} },
      subscription,
    );
    
    const [receivedRequest] = mockServer.requests;
    const isValid = verifyWebhookSignature(
      receivedRequest.body,
      receivedRequest.headers['x-webhook-signature'],
      'test-secret',
    );
    
    expect(isValid).toBe(true);
  });
});

For the event-driven architecture that dispatches these webhooks on state changes, the event-driven architecture guide covers domain event emission and handler registration. For the Celery task queue that backs webhook delivery in Python stacks, the Celery guide covers retry policies and task routing. The Claude Skills 360 bundle includes webhook skill sets covering HMAC signing, delivery retry engines, idempotent consumers, and fanout architecture. Start with the free tier to try webhook delivery system generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free