webhook-architecture
Build a reliable event delivery system with automatic retries, HMAC signature verification, and dead-letter queues so no webhook is ever lost
What this skill does
# Webhook Architecture
## Overview
Webhooks are HTTP callbacks used by commerce platforms (Shopify, Stripe) to push real-time event notifications to your application. Reliable webhook infrastructure requires: HMAC signature verification to prevent spoofed events, idempotent handlers that tolerate duplicate delivery, exponential backoff retry logic, and a dead-letter queue for events that exhaust all retries. This skill covers building a reliable webhook receiver and, for custom platforms, a webhook sender using the Outbox Pattern.
## When to Use This Skill
- When receiving webhooks from Shopify, Stripe, Square, or other platforms
- When debugging missed events or duplicate processing caused by webhook delivery issues
- When building a commerce platform or app that needs to notify external systems of events
- When designing event-driven architecture between commerce microservices
- When setting up webhook fanout (single event delivered to multiple consumers)
## Core Instructions
### Step 1: Determine your platform and what webhooks you need to handle
| Platform | Where Webhooks Are Configured | Most Important Topics to Subscribe |
|----------|------------------------------|-----------------------------------|
| **Shopify** | **Settings → Notifications → Webhooks** (or via Admin API) | `orders/create`, `orders/paid`, `orders/cancelled`, `inventory_levels/update`, `refunds/create` |
| **WooCommerce** | Install **WP Webhooks** plugin (free, wordpress.org) or use WooCommerce's built-in webhooks under **WooCommerce → Settings → Advanced → Webhooks** | Order status changes (processing, completed, refunded), stock updates |
| **BigCommerce** | **Advanced Settings → Legacy API Settings → Webhooks** or via API | `store/order/statusUpdated`, `store/product/inventory/updated`, `store/cart/abandoned` |
| **Custom / Headless** | Build your own webhook system | Use the Outbox Pattern for sending; HMAC verification + idempotency for receiving; see implementation below |
### Step 2: Platform-specific webhook setup
---
#### Shopify
**Register webhooks via the Shopify admin:**
1. Go to **Settings → Notifications** and scroll to **Webhooks**
2. Click **Create webhook**
3. Select the event topic (e.g., `Order creation`) and enter your endpoint URL
4. Choose **JSON** as the format
**Get your webhook secret for HMAC verification:**
The secret is shown when you create the webhook. Store it as an environment variable — Shopify signs each webhook with this secret using HMAC-SHA256.
**For apps using the Admin API**, register webhooks programmatically:
```typescript
const res = await fetch(`https://${shopDomain}/admin/api/2025-01/webhooks.json`, {
method: 'POST',
headers: { 'X-Shopify-Access-Token': accessToken, 'Content-Type': 'application/json' },
body: JSON.stringify({ webhook: {
topic: 'orders/create',
address: `${process.env.APP_URL}/api/webhooks/shopify/order-created`,
format: 'json',
}}),
});
```
---
#### WooCommerce
**Use WooCommerce's built-in webhooks:**
1. Go to **WooCommerce → Settings → Advanced → Webhooks**
2. Click **Add webhook**
3. Set **Name**, **Status: Active**, **Topic** (e.g., Order Created), and your **Delivery URL**
4. The **Secret** field generates an HMAC-SHA256 signature for each delivery — copy it for your endpoint's verification
**Or use WP Webhooks plugin for more control:**
1. Install **WP Webhooks** (free, wordpress.org) for advanced trigger conditions and payload customization
2. Go to **Settings → WP Webhooks** and configure triggers for WooCommerce order events
3. WP Webhooks supports retry logic and delivery logs out of the box
---
#### Custom / Headless
For custom storefronts, implement both reliable receiving (for incoming webhooks from Stripe, Shopify, etc.) and reliable sending (Outbox Pattern for notifying your own integrations).
**HMAC signature verification (Shopify and generic):**
```typescript
// lib/webhooks/verify.ts
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyShopifyWebhook(rawBody: Buffer, hmacHeader: string, secret: string): boolean {
const expected = createHmac('sha256', secret).update(rawBody).digest('base64');
const received = Buffer.from(hmacHeader);
const expectedBuffer = Buffer.from(expected);
if (received.length !== expectedBuffer.length) return false;
return timingSafeEqual(received, expectedBuffer);
}
export function verifyStripeWebhook(rawBody: Buffer, signatureHeader: string, secret: string): boolean {
const parts = signatureHeader.split(',');
const timestamp = parts.find(p => p.startsWith('t='))?.replace('t=', '');
const v1 = parts.find(p => p.startsWith('v1='))?.replace('v1=', '');
if (!timestamp || !v1) return false;
// Reject events older than 5 minutes (replay attack protection)
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) return false;
const expected = createHmac('sha256', secret)
.update(`${timestamp}.${rawBody.toString('utf8')}`)
.digest('hex');
return timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}
```
**Idempotent webhook receiver** — deduplicate using the platform's event ID:
```typescript
// app/api/webhooks/shopify/route.ts
export async function POST(req: NextRequest) {
const rawBody = Buffer.from(await req.arrayBuffer());
const hmac = req.headers.get('x-shopify-hmac-sha256') ?? '';
const topic = req.headers.get('x-shopify-topic') ?? '';
const eventId = req.headers.get('x-shopify-webhook-id') ?? '';
// 1. Verify signature — reject invalid requests immediately
if (!verifyShopifyWebhook(rawBody, hmac, process.env.SHOPIFY_WEBHOOK_SECRET!)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
// 2. Idempotency check — deduplicate by event ID
const alreadyProcessed = await db.processedWebhooks.exists(eventId);
if (alreadyProcessed) return NextResponse.json({ received: true, status: 'already_processed' });
// 3. Mark as received BEFORE processing (prevents duplicate on concurrent delivery)
await db.processedWebhooks.insert({ id: eventId, topic, receivedAt: new Date(), status: 'processing' });
// 4. Return 200 immediately, process asynchronously
processWebhookAsync(topic, rawBody, eventId); // Don't await — return fast
return NextResponse.json({ received: true });
}
async function processWebhookAsync(topic: string, rawBody: Buffer, eventId: string) {
try {
const payload = JSON.parse(rawBody.toString('utf8'));
switch (topic) {
case 'orders/create': await importOrder(payload); break;
case 'orders/cancelled': await cancelOrder(payload.id); break;
case 'inventory_levels/update': await syncInventory(payload); break;
}
await db.processedWebhooks.update(eventId, { status: 'processed', processedAt: new Date() });
} catch (err: any) {
await db.processedWebhooks.update(eventId, { status: 'failed', error: err.message });
}
}
```
**Outbox Pattern for reliable webhook sending** — guarantees at-least-once delivery even if your sender crashes:
```typescript
// lib/webhooks/outbox.ts
// Write to outbox in the SAME transaction as the business event
export async function publishEvent(trx: Transaction, eventType: string, payload: object) {
await trx.webhookOutbox.insert({
id: crypto.randomUUID(),
eventType,
payload: JSON.stringify(payload),
status: 'pending',
attempts: 0,
nextRetryAt: new Date(),
});
}
// Outbox poller — runs every 10 seconds, separate from your main app
export async function processOutbox() {
const pending = await db.webhookOutbox.findPending({ status: ['pending', 'retrying'], nextRetryAt: { $lte: new Date() }, limit: 100 });
for (const event of pending) await deliverEvent(event);
}
// Retry schedule: 1min, 5min, 30min, 2hr, 8hr → DLQ after 5 attempts
const RETRY_DELAYS_MS = [60_000, 300_000, 1_800_000, 7_200_000, 28_800_000];
async function handleDeliveryFailure(event: OutboxEvent, error: string) {
const nextAttemptsRelated in integrations-apis
product-information-management
IncludedCentralize product data in a PIM system like Akeneo or Salsify and syndicate enriched content to all your sales channels automatically
pos-integration
IncludedConnect your physical point-of-sale system to your online store for unified inventory, shared customer records, and omnichannel order management
email-service-integration
IncludedSend reliable transactional emails (order confirmations, shipping updates) via SendGrid, SES, or Postmark with templates and deliverability best practices
analytics-integration
IncludedImplement GA4, Meta Pixel, and server-side tagging with a proper data layer so you capture accurate conversion events for ad campaigns
erp-integration
IncludedSync orders, inventory, and customer data between your store and ERP systems like SAP, NetSuite, or Odoo using middleware and async queues
marketplace-connectors
IncludedList products on Amazon, eBay, and Walmart with two-way inventory sync, automated listing creation, and order import into your store