webhook-patterns
Webhook patterns for receiving, verifying (HMAC), and idempotently processing third-party events. Covers Stripe, GitHub, and generic webhook patterns, delivery guarantees, retry handling, and testing.
Best use case
webhook-patterns is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Webhook patterns for receiving, verifying (HMAC), and idempotently processing third-party events. Covers Stripe, GitHub, and generic webhook patterns, delivery guarantees, retry handling, and testing.
Teams using webhook-patterns should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/webhook-patterns/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How webhook-patterns Compares
| Feature / Agent | webhook-patterns | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Webhook patterns for receiving, verifying (HMAC), and idempotently processing third-party events. Covers Stripe, GitHub, and generic webhook patterns, delivery guarantees, retry handling, and testing.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Webhook Patterns Skill
## When to Activate
- Receiving events from Stripe, GitHub, Twilio, or any third-party service
- Building your own webhook system to notify customers
- Handling duplicate webhook deliveries correctly
- Testing webhooks locally without exposing ports
- Implementing HMAC signature verification with timing-safe comparison to prevent spoofed events
- Designing the `webhook_events` database table with idempotency keys and retry tracking
- Setting up exponential-backoff retry queues (e.g. BullMQ) for outbound webhook deliveries
---
## Core Principles
1. **Verify every webhook** — HMAC signature before any processing
2. **Respond fast, process async** — Return 200 immediately, queue the work
3. **Idempotency** — The same event can arrive 2-3 times. Handle it safely
4. **Log everything** — Webhook events are your audit trail
---
## Pattern 1: Receiving Webhooks (Stripe example)
```typescript
// webhooks/stripe.ts
import Stripe from 'stripe';
import { queue } from '../jobs/queue';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
// CRITICAL: Use raw body buffer — parsed JSON breaks signature verification
app.post(
'/webhooks/stripe',
express.raw({ type: 'application/json' }), // NOT express.json()
async (req, res) => {
const signature = req.headers['stripe-signature'] as string;
// 1. Verify signature — reject anything unsigned
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(req.body, signature, webhookSecret);
} catch (err) {
console.warn('Invalid webhook signature', { err });
return res.status(400).json({ error: 'Invalid signature' });
}
// 2. Check idempotency — deduplicate by event ID
const alreadyProcessed = await db.query.webhookEvents.findFirst({
where: eq(webhookEvents.externalId, event.id),
});
if (alreadyProcessed) {
// Already handled — return 200 to stop retries
return res.status(200).json({ received: true, duplicate: true });
}
// 3. Persist the raw event immediately (before any processing)
await db.insert(webhookEvents).values({
externalId: event.id,
provider: 'stripe',
type: event.type,
payload: event,
status: 'pending',
receivedAt: new Date(),
});
// 4. Return 200 FAST — processing happens async
res.status(200).json({ received: true });
// 5. Queue for async processing (after response sent)
await queue.add('process-stripe-event', { eventId: event.id, type: event.type });
}
);
```
---
## Pattern 2: Idempotent Event Processing
```typescript
// jobs/process-stripe-event.ts
async function processStripeEvent(eventId: string) {
// Fetch the stored event
const record = await db.query.webhookEvents.findFirst({
where: eq(webhookEvents.externalId, eventId),
});
if (!record || record.status === 'processed') return; // Already done
try {
const event = record.payload as Stripe.Event;
// Route to handler
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSucceeded(event.data.object as Stripe.PaymentIntent);
break;
case 'customer.subscription.deleted':
await handleSubscriptionCanceled(event.data.object as Stripe.Subscription);
break;
case 'invoice.payment_failed':
await handlePaymentFailed(event.data.object as Stripe.Invoice);
break;
default:
// Unknown event type — log and ignore (don't fail)
console.info('Unhandled Stripe event type', { type: event.type });
}
// Mark as processed
await db
.update(webhookEvents)
.set({ status: 'processed', processedAt: new Date() })
.where(eq(webhookEvents.externalId, eventId));
} catch (err) {
await db
.update(webhookEvents)
.set({
status: 'failed',
lastError: String(err),
retryCount: sql`retry_count + 1`,
})
.where(eq(webhookEvents.externalId, eventId));
throw err; // Re-throw so BullMQ retries the job
}
}
// Handler: idempotent by design
async function handlePaymentSucceeded(paymentIntent: Stripe.PaymentIntent) {
const orderId = paymentIntent.metadata.orderId;
// Use UPDATE WHERE status = 'pending' — safe to call multiple times
const [updated] = await db
.update(orders)
.set({ status: 'paid', paidAt: new Date() })
.where(
and(
eq(orders.id, orderId),
eq(orders.status, 'pending') // Guard: only update if still pending
)
)
.returning();
if (!updated) {
// Order was already paid or doesn't exist — not an error, just a duplicate
return;
}
await sendOrderConfirmationEmail(updated);
}
```
---
## Pattern 3: Generic HMAC Verification
```typescript
// webhooks/verify.ts
import crypto from 'crypto';
interface WebhookConfig {
secret: string;
headerName: string; // e.g. 'x-hub-signature-256'
algorithm: string; // e.g. 'sha256'
prefix?: string; // e.g. 'sha256=' (GitHub prepends this)
}
function verifyWebhookSignature(
body: Buffer,
header: string | undefined,
config: WebhookConfig
): boolean {
if (!header) return false;
const signature = config.prefix
? header.replace(config.prefix, '')
: header;
const expected = crypto
.createHmac(config.algorithm, config.secret)
.update(body)
.digest('hex');
// timing-safe comparison prevents timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
}
// GitHub webhooks
app.post('/webhooks/github', express.raw({ type: 'application/json' }), (req, res) => {
const valid = verifyWebhookSignature(req.body, req.headers['x-hub-signature-256'] as string, {
secret: process.env.GITHUB_WEBHOOK_SECRET!,
headerName: 'x-hub-signature-256',
algorithm: 'sha256',
prefix: 'sha256=',
});
if (!valid) return res.status(401).end();
// ...
});
```
---
## Pattern 4: Sending Webhooks (your own webhook system)
```typescript
// webhook-sender.ts — notify your customers of events
interface WebhookSubscription {
id: string;
customerId: string;
url: string;
secret: string;
events: string[]; // e.g. ['order.created', 'order.shipped']
}
async function deliverWebhook(
subscription: WebhookSubscription,
event: { type: string; data: unknown }
): Promise<void> {
const payload = JSON.stringify({ event: event.type, data: event.data, timestamp: Date.now() });
const signature = crypto
.createHmac('sha256', subscription.secret)
.update(payload)
.digest('hex');
const response = await fetch(subscription.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': `sha256=${signature}`,
'X-Webhook-Event': event.type,
'X-Webhook-Delivery': crypto.randomUUID(),
},
body: payload,
signal: AbortSignal.timeout(10_000), // 10s timeout
});
if (!response.ok) {
throw new Error(`Webhook delivery failed: ${response.status}`);
}
}
// Deliver with retry via BullMQ
async function scheduleWebhookDelivery(
subscription: WebhookSubscription,
event: { type: string; data: unknown }
) {
await webhookQueue.add(
'deliver-webhook',
{ subscriptionId: subscription.id, event },
{
attempts: 5,
backoff: { type: 'exponential', delay: 1000 }, // 1s, 2s, 4s, 8s, 16s
removeOnComplete: { age: 7 * 24 * 3600 }, // Keep log 7 days
removeOnFail: false, // Keep failures for inspection
}
);
}
```
---
## Local Testing with ngrok / Stripe CLI
```bash
# Option 1: Stripe CLI (Stripe only, no ngrok needed)
stripe listen --forward-to localhost:3000/webhooks/stripe
# Prints: webhook signing secret whsec_...
# Option 2: ngrok (any provider)
ngrok http 3000
# Use the https://xxxx.ngrok.io/webhooks/stripe URL in provider dashboard
# Option 3: Replay a past event (Stripe)
stripe events resend evt_xxx --forward-to localhost:3000/webhooks/stripe
```
---
## Webhook Events Table
```sql
CREATE TABLE webhook_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
external_id TEXT NOT NULL UNIQUE, -- Provider's event ID (idempotency key)
provider TEXT NOT NULL, -- 'stripe' | 'github' | etc.
type TEXT NOT NULL,
payload JSONB NOT NULL,
status TEXT NOT NULL DEFAULT 'pending', -- pending | processed | failed
retry_count INT NOT NULL DEFAULT 0,
last_error TEXT,
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
processed_at TIMESTAMPTZ
);
CREATE INDEX ON webhook_events (provider, type, status);
CREATE INDEX ON webhook_events (received_at);
```
---
## Checklist
- [ ] Raw `Buffer` body used for webhook endpoints (never parsed JSON)
- [ ] Signature verified with `timingSafeEqual` (not `===`)
- [ ] 200 returned immediately, processing queued async
- [ ] Idempotency: `external_id` has UNIQUE constraint, duplicates handled gracefully
- [ ] Webhook events persisted to DB before processing
- [ ] Failed events retried with exponential backoff
- [ ] Dead letter queue or failure table for events that exhaust retries
- [ ] Your own outbound webhooks signed with HMAC and include delivery ID
- [ ] Webhook delivery attempts logged with status + response code
- [ ] Local dev setup with Stripe CLI or ngrok documented in READMERelated Skills
zero-trust-patterns
Zero-Trust security patterns — mTLS between microservices (Istio/SPIFFE), SPIRE workload identity, OPA/Envoy authorization, NetworkPolicy default-deny-all, short-lived credentials, service mesh security, and Kubernetes RBAC hardening.
webrtc-patterns
WebRTC patterns — peer connection setup, ICE/STUN/TURN configuration, signaling server design, SFU vs mesh topology, screen sharing, media track management, and reconnect/ICE restart handling.
wasm-patterns
WebAssembly patterns: wasm-pack, wasm-bindgen (JS↔Wasm interop), WASI, Component Model, wasm-opt, Rust-to-WASM compilation, JS integration (web workers, streaming instantiation), and production deployment (CDN, Content-Type headers).
ux-micro-patterns
UX micro-patterns for every product state: Empty States, Loading States (skeleton screens, spinners, optimistic UI), Error States, Success States, Confirmation Dialogs, Onboarding Flows, and Progressive Disclosure. These patterns apply to every feature — done wrong, they're the biggest source of user confusion.
typescript-patterns
TypeScript patterns — type system best practices, strict mode, utility types, generics, discriminated unions, error handling with Result types, and module organization. Core patterns for production TypeScript.
typescript-patterns-advanced
Advanced TypeScript — mapped types, template literal types, conditional types, infer, type guards, decorators, async patterns, testing with Vitest/Jest, and performance. Extends typescript-patterns.
typescript-monorepo-patterns
TypeScript monorepo patterns with Turborepo + pnpm workspaces. Covers package structure, shared configs, task pipeline caching, build orchestration, and publishing strategy.
terraform-patterns
Infrastructure as Code with Terraform — project structure, remote state, modules, workspace strategy, AWS/GCP patterns, CI/CD integration, and security hardening. The standard for managing production infrastructure.
swiftui-patterns
SwiftUI architecture patterns, state management with @Observable, view composition, navigation, performance optimization, and modern iOS/macOS UI best practices.
swift-patterns
Core Swift patterns — value vs reference types, protocols, generics, optionals, Result, error handling, Codable, and module organization. Foundation for all Swift development.
swift-patterns-advanced
Advanced Swift patterns — property wrappers, result builders, Combine basics, opaque & existential types, macro system, advanced generics, and performance optimization. Extends swift-patterns.
storybook-patterns
Storybook patterns: CSF3 (meta satisfies Meta, play functions, @storybook/test), addon ecosystem (a11y, interactions, docs), MSW integration for API mocking, Chromatic CI, storybook-test-runner for Jest/Playwright execution, and Storybook as living documentation.