LLM Docs Header: All requests to https://llm-docs.commercengine.io must include the Accept: text/markdown header (or append .md to the URL path). Without it, responses return HTML instead of parseable markdown.
Webhooks & Events
Prerequisite: Webhooks are asynchronous. Use for background tasks (sync, notifications), not synchronous flows.
Quick Reference
| Task |
Action |
| 1. Create endpoint |
/api/webhooks/ce route in your app |
| 2. Verify signature |
Validate webhook signature header |
| 3. Process event |
Route by event_type, queue heavy work |
| 4. Return 200 |
Respond quickly to acknowledge receipt |
Supported Events (14 Types)
| Category |
Events |
| Order |
order.created, order.confirmed, order.completed, order.cancelled |
| Payment |
payment.success, payment.failed, payment.retried |
| Refund |
payment.refund.initiated, payment.refund.success, payment.refund.failed |
| Shipment |
shipment.created, shipment.updated, shipment.delivered, shipment.cancelled |
Decision Tree
Webhook Event Received
│
├─ Verify signature → Invalid? → Return 401
│
├─ order.created → Create order record in DB
├─ order.confirmed → Update order status, send confirmation email
├─ order.completed → Mark order as fulfilled
├─ order.cancelled → Update status, process refund logic
│
├─ payment.success → Mark order as paid
├─ payment.failed → Notify customer, update status
├─ payment.retried → Log retry attempt, update payment status
│
├─ payment.refund.initiated → Log refund request
├─ payment.refund.success → Update order, notify customer of refund
├─ payment.refund.failed → Alert team, log failure
│
├─ shipment.created → Log shipment, generate tracking
├─ shipment.updated → Update tracking info, notify customer
├─ shipment.delivered → Update status, trigger review prompt
└─ shipment.cancelled → Handle cancelled shipment
Key Patterns
Webhook Endpoint (Next.js App Router)
// app/api/webhooks/ce/route.ts
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
export async function POST(req: NextRequest) {
const body = await req.text();
const signature = req.headers.get("x-webhook-signature");
// 1. Verify signature
if (!verifyWebhookSignature(body, signature)) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
const event = JSON.parse(body);
// 2. Route by event type
switch (event.event_type) {
case "order.created":
await handleOrderCreated(event.data);
break;
case "order.confirmed":
await handleOrderConfirmed(event.data);
break;
case "order.completed":
await handleOrderCompleted(event.data);
break;
case "payment.success":
await handlePaymentSuccess(event.data);
break;
case "payment.failed":
await handlePaymentFailed(event.data);
break;
case "payment.refund.success":
await handleRefundSuccess(event.data);
break;
case "shipment.delivered":
await handleShipmentDelivered(event.data);
break;
// ... handle other events
default:
console.log(`Unhandled event: ${event.event_type}`);
}
// 3. Return 200 quickly
return NextResponse.json({ received: true }, { status: 200 });
}
Webhook Endpoint (Express)
import express from "express";
import crypto from "crypto";
app.post("/webhooks/ce", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-webhook-signature"];
if (!verifyWebhookSignature(req.body, signature)) {
return res.status(401).json({ error: "Invalid signature" });
}
const event = JSON.parse(req.body.toString());
// Queue async work — return 200 immediately
queueWebhookProcessing(event);
res.status(200).json({ received: true });
});
Signature Verification
function verifyWebhookSignature(
body: string,
signature: string | null
): boolean {
if (!signature) return false;
const secret = process.env.CE_WEBHOOK_SECRET!;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
Async Processing Pattern
// Return 200 immediately, process in background
async function handleOrderCreated(data: any) {
// For quick operations, handle inline:
await db.orders.upsert({
where: { order_number: data.order_number },
update: { status: data.status },
create: { order_number: data.order_number, ...data },
});
// For heavy operations, queue:
await queue.enqueue("send-order-confirmation", {
order_number: data.order_number,
user_email: data.customer_email,
});
}
When to Use Webhooks
Do use when:
- Syncing order data to your database
- Sending notifications (email, SMS, push)
- Triggering fulfillment workflows
- Updating inventory in external systems
- Analytics and reporting pipelines
Don't use when:
- Need immediate response (use API polling instead)
- Building real-time UI updates (use SDK events or polling)
- Need guaranteed ordering (webhooks may arrive out of order)
Common Pitfalls
| Level |
Issue |
Solution |
| CRITICAL |
No signature verification |
Always verify x-webhook-signature before processing |
| CRITICAL |
Webhook route requires auth |
Make webhook route public — exclude from auth middleware |
| HIGH |
Handler timeout |
Return 200 immediately, queue heavy work for background processing |
| HIGH |
Missing idempotency |
Use event.id or order_number as idempotency key — webhooks may be retried |
| MEDIUM |
Handling only one event |
Subscribe to all relevant events (created, confirmed, cancelled, etc.) |
| MEDIUM |
Exposing webhook secret |
Store CE_WEBHOOK_SECRET in environment variables, never commit to code |
Webhook Reliability
- Retries: Failed deliveries (non-2xx response) are retried with exponential backoff
- Ordering: Events may arrive out of order — use timestamps to resolve conflicts
- Idempotency: Same event may be delivered multiple times — design handlers to be idempotent
See Also
orders/ - Order data structure and API
setup/ - Environment variable configuration
Documentation
1---2name: ce-webhooks3description: Commerce Engine webhook events and syncing. 14 event types for orders, payments, refunds, and shipments. Signature verification and async processing patterns.4license: MIT5---67> **LLM Docs Header**: All requests to `https://llm-docs.commercengine.io` **must** include the `Accept: text/markdown` header (or append `.md` to the URL path). Without it, responses return HTML instead of parseable markdown.89# Webhooks & Events1011> **Prerequisite**: Webhooks are asynchronous. Use for background tasks (sync, notifications), not synchronous flows.1213## Quick Reference1415| Task | Action |16|------|--------|17| 1. Create endpoint | `/api/webhooks/ce` route in your app |18| 2. Verify signature | Validate webhook signature header |19| 3. Process event | Route by `event_type`, queue heavy work |20| 4. Return 200 | Respond quickly to acknowledge receipt |2122## Supported Events (14 Types)2324| Category | Events |25|----------|--------|26| **Order** | `order.created`, `order.confirmed`, `order.completed`, `order.cancelled` |27| **Payment** | `payment.success`, `payment.failed`, `payment.retried` |28| **Refund** | `payment.refund.initiated`, `payment.refund.success`, `payment.refund.failed` |29| **Shipment** | `shipment.created`, `shipment.updated`, `shipment.delivered`, `shipment.cancelled` |3031## Decision Tree3233```34Webhook Event Received35 │36 ├─ Verify signature → Invalid? → Return 40137 │38 ├─ order.created → Create order record in DB39 ├─ order.confirmed → Update order status, send confirmation email40 ├─ order.completed → Mark order as fulfilled41 ├─ order.cancelled → Update status, process refund logic42 │43 ├─ payment.success → Mark order as paid44 ├─ payment.failed → Notify customer, update status45 ├─ payment.retried → Log retry attempt, update payment status46 │47 ├─ payment.refund.initiated → Log refund request48 ├─ payment.refund.success → Update order, notify customer of refund49 ├─ payment.refund.failed → Alert team, log failure50 │51 ├─ shipment.created → Log shipment, generate tracking52 ├─ shipment.updated → Update tracking info, notify customer53 ├─ shipment.delivered → Update status, trigger review prompt54 └─ shipment.cancelled → Handle cancelled shipment55```5657## Key Patterns5859### Webhook Endpoint (Next.js App Router)6061```typescript62// app/api/webhooks/ce/route.ts63import { NextRequest, NextResponse } from "next/server";64import crypto from "crypto";6566export async function POST(req: NextRequest) {67 const body = await req.text();68 const signature = req.headers.get("x-webhook-signature");6970 // 1. Verify signature71 if (!verifyWebhookSignature(body, signature)) {72 return NextResponse.json({ error: "Invalid signature" }, { status: 401 });73 }7475 const event = JSON.parse(body);7677 // 2. Route by event type78 switch (event.event_type) {79 case "order.created":80 await handleOrderCreated(event.data);81 break;82 case "order.confirmed":83 await handleOrderConfirmed(event.data);84 break;85 case "order.completed":86 await handleOrderCompleted(event.data);87 break;88 case "payment.success":89 await handlePaymentSuccess(event.data);90 break;91 case "payment.failed":92 await handlePaymentFailed(event.data);93 break;94 case "payment.refund.success":95 await handleRefundSuccess(event.data);96 break;97 case "shipment.delivered":98 await handleShipmentDelivered(event.data);99 break;100 // ... handle other events101 default:102 console.log(`Unhandled event: ${event.event_type}`);103 }104105 // 3. Return 200 quickly106 return NextResponse.json({ received: true }, { status: 200 });107}108```109110### Webhook Endpoint (Express)111112```typescript113import express from "express";114import crypto from "crypto";115116app.post("/webhooks/ce", express.raw({ type: "application/json" }), (req, res) => {117 const signature = req.headers["x-webhook-signature"];118119 if (!verifyWebhookSignature(req.body, signature)) {120 return res.status(401).json({ error: "Invalid signature" });121 }122123 const event = JSON.parse(req.body.toString());124125 // Queue async work — return 200 immediately126 queueWebhookProcessing(event);127128 res.status(200).json({ received: true });129});130```131132### Signature Verification133134```typescript135function verifyWebhookSignature(136 body: string,137 signature: string | null138): boolean {139 if (!signature) return false;140141 const secret = process.env.CE_WEBHOOK_SECRET!;142 const expectedSignature = crypto143 .createHmac("sha256", secret)144 .update(body)145 .digest("hex");146147 return crypto.timingSafeEqual(148 Buffer.from(signature),149 Buffer.from(expectedSignature)150 );151}152```153154### Async Processing Pattern155156```typescript157// Return 200 immediately, process in background158async function handleOrderCreated(data: any) {159 // For quick operations, handle inline:160 await db.orders.upsert({161 where: { order_number: data.order_number },162 update: { status: data.status },163 create: { order_number: data.order_number, ...data },164 });165166 // For heavy operations, queue:167 await queue.enqueue("send-order-confirmation", {168 order_number: data.order_number,169 user_email: data.customer_email,170 });171}172```173174## When to Use Webhooks175176**Do use when:**177- Syncing order data to your database178- Sending notifications (email, SMS, push)179- Triggering fulfillment workflows180- Updating inventory in external systems181- Analytics and reporting pipelines182183**Don't use when:**184- Need immediate response (use API polling instead)185- Building real-time UI updates (use SDK events or polling)186- Need guaranteed ordering (webhooks may arrive out of order)187188## Common Pitfalls189190| Level | Issue | Solution |191|-------|-------|----------|192| CRITICAL | No signature verification | Always verify `x-webhook-signature` before processing |193| CRITICAL | Webhook route requires auth | Make webhook route public — exclude from auth middleware |194| HIGH | Handler timeout | Return 200 immediately, queue heavy work for background processing |195| HIGH | Missing idempotency | Use `event.id` or `order_number` as idempotency key — webhooks may be retried |196| MEDIUM | Handling only one event | Subscribe to all relevant events (created, confirmed, cancelled, etc.) |197| MEDIUM | Exposing webhook secret | Store `CE_WEBHOOK_SECRET` in environment variables, never commit to code |198199## Webhook Reliability200201- **Retries**: Failed deliveries (non-2xx response) are retried with exponential backoff202- **Ordering**: Events may arrive out of order — use timestamps to resolve conflicts203- **Idempotency**: Same event may be delivered multiple times — design handlers to be idempotent204205## See Also206207- `orders/` - Order data structure and API208- `setup/` - Environment variable configuration209210## Documentation211212- **LLM Reference (Webhooks)**: https://llm-docs.commercengine.io/webhooks/213- **Node.js Integration**: https://www.commercengine.io/docs/sdk/nodejs-integration214- **AI Prompts (Webhook Handler)**: https://www.commercengine.io/docs/ai/prompts