Shopify Webhooks
Shopify webhook handling for order updates, fulfillment tracking, and status sync
Shopify Webhooks
Shopify sends webhooks to notify the platform of order updates, fulfillment changes, and cancellations. The webhook handler at app/api/webhooks/shopify/route.ts verifies signatures and syncs Shopify state back to the Prisma database.
Endpoint
POST /api/webhooks/shopifySignature Verification
File: lib/shopify/webhook.ts
Shopify signs webhook payloads with HMAC-SHA256. The handler verifies the x-shopify-hmac-sha256 header using crypto.timingSafeEqual() to prevent timing attacks:
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('base64')
// Constant-time comparison
crypto.timingSafeEqual(
Buffer.from(expected, 'utf8'),
Buffer.from(signature, 'utf8')
)The webhook secret is read from SHOPIFY_WEBHOOK_SECRET.
Invalid signatures return HTTP 401. Missing topic headers return HTTP 400.
Handled Topics
| Topic | Handler |
|---|---|
orders/create | handleOrderPayload() |
orders/updated | handleOrderPayload() |
orders/paid | handleOrderPayload() |
orders/cancelled | handleOrderPayload() |
fulfillments/create | handleFulfillmentPayload() |
fulfillments/update | handleFulfillmentPayload() |
Unhandled topics are logged and ignored with a 200 response.
Order Matching
Orders are matched by two identifiers:
shopifyOrderId-- Searched first viaprisma.order.findFirst()orderNumber-- Extracted fromnote_attributesand searched viaprisma.order.findUnique()
The orderNumber is embedded during order sync (see Shopify Integration).
Order Updates (handleOrderPayload)
When an order webhook arrives, the handler updates:
| Shopify Field | Prisma Field | Notes |
|---|---|---|
id | shopifyOrderId | Stored as string |
name | shopifyOrderName | e.g., "#1234" |
financial_status | paymentStatus | Mapped via mapShopifyFinancialStatusToPrisma() |
fulfillment_status | status | Mapped via mapShopifyFulfillmentStatusToPrisma() |
cancelled_at | status: CANCELLED | Also updates payment status |
fulfillments[].tracking_number | trackingNumber | From latest fulfillment |
Financial Status Mapping
| Shopify Status | Prisma PaymentStatus |
|---|---|
pending / authorized / partially_paid | PENDING |
paid | PAID |
partially_refunded | PARTIALLY_REFUNDED |
refunded | REFUNDED |
voided | FAILED |
Fulfillment Status Mapping
| Shopify Status | Prisma OrderStatus |
|---|---|
fulfilled | SHIPPED |
partial / restocked | PROCESSING |
cancelled | CANCELLED |
| (null/undefined) | PENDING |
Timestamp Updates
shippedAt: Set when fulfillment_status becomesfulfilledandshippedAtwas previously nulldeliveredAt: Set whenclosed_atis present anddeliveredAtwas previously null
Fulfillment Updates (handleFulfillmentPayload)
Fulfillment-specific webhooks update:
trackingNumberfromtracking_numberortracking_numbers[0]shopifyFulfillmentStatus- Order status:
SHIPPEDfor active fulfillments,CANCELLEDfor cancelled ones shippedAtanddeliveredAttimestamps
Error Handling
- Unknown orders are logged as warnings but return
200(acknowledged) - Processing errors return
500with a generic error message - All database operations are individual updates (not transactions) for resilience
Key Files
| File | Purpose |
|---|---|
app/api/webhooks/shopify/route.ts | Webhook HTTP endpoint and routing |
lib/shopify/webhook.ts | Signature verification and status mapping |
How is this guide?
Last updated on