webhook-architecture

Build a reliable event delivery system with automatic retries, HMAC signature verification, and dead-letter queues so no webhook is ever lost

11 stars

Best use case

webhook-architecture is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Build a reliable event delivery system with automatic retries, HMAC signature verification, and dead-letter queues so no webhook is ever lost

Teams using webhook-architecture 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

$curl -o ~/.claude/skills/webhook-architecture/SKILL.md --create-dirs "https://raw.githubusercontent.com/finsilabs/awesome-ecommerce-skills/main/skills/integrations-apis/webhook-architecture/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/webhook-architecture/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How webhook-architecture Compares

Feature / Agentwebhook-architectureStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Build a reliable event delivery system with automatic retries, HMAC signature verification, and dead-letter queues so no webhook is ever lost

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 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 nextAttempts = event.attempts + 1;

  if (nextAttempts >= RETRY_DELAYS_MS.length) {
    await db.webhookOutbox.update(event.id, { status: 'dead_letter', lastError: error });
    await db.webhookDeadLetters.insert({ eventId: event.id, failedAt: new Date(), reason: error });
    await alertOpsTeam(`Webhook permanently failed after ${nextAttempts} attempts`, { eventType: event.eventType, error });
  } else {
    const nextRetryAt = new Date(Date.now() + RETRY_DELAYS_MS[nextAttempts - 1]);
    await db.webhookOutbox.update(event.id, { status: 'retrying', attempts: nextAttempts, nextRetryAt, lastError: error });
  }
}
```

**Replay dead-letter events** (after fixing the subscriber endpoint):

```typescript
export async function replayDeadLetter(deadLetterId: string) {
  const deadLetter = await db.webhookDeadLetters.findById(deadLetterId);
  await db.webhookOutbox.update(deadLetter.eventId, {
    status: 'pending', attempts: 0, nextRetryAt: new Date(),
  });
  await db.webhookDeadLetters.update(deadLetterId, { replayedAt: new Date() });
}
```

## Best Practices

- **Always return 2xx immediately** — a slow webhook handler blocks delivery and may cause the sender to time out and retry; enqueue events on receipt and process asynchronously
- **Use the Outbox Pattern for reliable sending** — writing to an outbox table in the same DB transaction as your domain event guarantees at-least-once delivery even if your webhook sender crashes
- **Make handlers idempotent** — use the event's unique ID to deduplicate; "at-least-once" delivery is the standard for all webhook platforms; you must tolerate receiving the same event twice
- **Log every delivery attempt** — store delivery attempts with response codes, timing, and errors; this is essential for debugging and provides audit evidence for compliance
- **Implement a dead-letter queue with alerting** — events that exhaust retries need human intervention; alert via Slack/PagerDuty and provide a replay mechanism

## Common Pitfalls

| Problem | Solution |
|---------|----------|
| Duplicate order processing from retried webhooks | Implement idempotency using the webhook event ID as a unique key in a `processed_webhooks` table with a TTL of 30 days |
| Webhook handler times out causing retries | Process webhooks async: write to queue on receipt, return 200 immediately, process from queue |
| WooCommerce webhook delivery failures | Check the **WooCommerce → System Status → Logs** for delivery errors; common causes are SSL certificate issues and timeout on slow shared hosting |
| Shopify webhook HMAC mismatch | Compute HMAC over the raw request body; do NOT parse the JSON first — body parsers may reformat the JSON and change the signature |
| Dead letters pile up silently | Alert when the dead letter count exceeds a threshold (e.g., 10 events); dead letters indicate a systematic subscriber failure requiring investigation |

## Related Skills

- @analytics-integration
- @erp-integration
- @marketplace-connectors
- @monitoring-alerting-commerce

Related Skills

shopify-webhooks

11
from finsilabs/awesome-ecommerce-skills

Register, verify, and reliably process Shopify webhook events for orders, inventory, and customers with HMAC validation and idempotency handling

wishlist-save-for-later

11
from finsilabs/awesome-ecommerce-skills

Let shoppers save products to a wishlist, share it with friends, and get notified when saved items come back in stock or drop in price

storefront-theming

11
from finsilabs/awesome-ecommerce-skills

Build a themeable storefront with design tokens and CSS custom properties that supports white-labeling, multi-brand variants, and dark mode

search-autocomplete

11
from finsilabs/awesome-ecommerce-skills

Speed up product discovery with instant search suggestions, fuzzy typo matching, and category-aware results powered by Algolia or Elasticsearch

responsive-storefront

11
from finsilabs/awesome-ecommerce-skills

Build a mobile-first storefront with thumb-friendly navigation, sticky add-to-cart buttons, and touch-optimized components for high mobile conversion

recently-viewed-products

11
from finsilabs/awesome-ecommerce-skills

Show shoppers the products they recently browsed using browser storage so they can easily pick up where they left off on your store

quick-view-modal

11
from finsilabs/awesome-ecommerce-skills

Let shoppers preview product details and add items to cart from the listing page without navigating away, reducing friction in the shopping flow

product-page-design

11
from finsilabs/awesome-ecommerce-skills

Design high-converting product detail pages with image galleries, variant selectors, social proof, and clear calls-to-action that drive add-to-cart

product-comparison

11
from finsilabs/awesome-ecommerce-skills

Let shoppers select multiple products and compare them side-by-side in a table with highlighted differences to help them make the right buying decision

mega-menu-builder

11
from finsilabs/awesome-ecommerce-skills

Build a rich navigation mega menu with product images, category highlights, featured banners, and keyboard-accessible dropdowns for large catalogs

image-zoom-360

11
from finsilabs/awesome-ecommerce-skills

Boost product confidence with high-res image zoom, 360-degree spin views, and inline video so shoppers can examine products closely before buying

faceted-navigation

11
from finsilabs/awesome-ecommerce-skills

Let shoppers filter products by multiple attributes simultaneously with URL-shareable filter state, instant results, and mobile-friendly controls