← Shopify App BuilderCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Shopify App Builder
Snapshot Sep 30, 2026 · 23:13 UTC · version 1.4.1
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "Webhook delivery methods, verification, retry behavior, payload handling, and implementation patterns for Shopify events. Triggers include: 'set up webhook', 'verify webhook signature', 'webhook delivery', 'HMAC verification', 'webhook retry', 'event subscription', 'webhook payload', 'AWS EventBridge Shopify', 'Google Pub/Sub webhook', 'webhook manifest'.",
"included_files": [],
"name": "webhooks",
"skill_md_contents": "---\nname: webhooks\ndescription: \"Webhook delivery methods, verification, retry behavior, payload handling, and implementation patterns for Shopify events. Triggers include: 'set up webhook', 'verify webhook signature', 'webhook delivery', 'HMAC verification', 'webhook retry', 'event subscription', 'webhook payload', 'AWS EventBridge Shopify', 'Google Pub/Sub webhook', 'webhook manifest'.\"\n---\n\n# Shopify Webhooks Implementation Guide\n\n## When to Use This Skill\n\nUse webhooks when you need to:\n- Receive real-time event notifications from Shopify (orders, products, customers, fulfillments, etc.)\n- Sync external systems with Shopify data changes\n- Trigger automated workflows based on shop events\n- Monitor GDPR compliance actions (customer data erasure, shop deletion)\n- Process bulk operations completion\n- Update inventory or pricing in third-party systems\n\n## Webhook Delivery Methods\n\n### 1. HTTPS (Traditional)\n\nMost common delivery method. Webhooks sent as POST requests to your publicly accessible HTTPS endpoint.\n\n**Characteristics:**\n- Requires public HTTPS endpoint (443, TLS 1.2+)\n- Real-time delivery (within seconds)\n- Max 5 retries over 48 hours (exponential backoff: 10s, 30s, 1m, 2m, 8h)\n- Request timeout: 30 seconds\n- Payload size: max 2MB\n- Rate limiting: respect Shopify API rate limits\n\n**Configuration Example:**\n```json\n{\n \"deliveryMethod\": {\n \"https\": {\n \"address\": \"https://myapp.example.com/webhooks/shopify\"\n }\n },\n \"query\": \"subscription { event { id occurredAt } }\",\n \"filter\": \"orders/created\"\n}\n```\n\n### 2. AWS EventBridge\n\nAsynchronous delivery via AWS EventBridge. Events queued in partner event bus.\n\n**Characteristics:**\n- No public endpoint needed\n- Queued delivery (managed by EventBridge)\n- Scalable, event-sourcing ready\n- Requires AWS EventBridge partner event bus setup\n- Lower operational overhead\n- Better for high-volume events\n\n**Setup Steps:**\n```bash\n# 1. Shopify creates AWS partner event bus in your account\n# 2. Your app subscribes to webhooks via EventBridge configuration\n# 3. Events appear in partner event bus: aws.partner/shopify.com/[app-id]/[account-id]\n\n# 3. Create rule to route to your targets (SQS, Lambda, etc.)\naws events put-rule \\\n --name shopify-webhook-router \\\n --event-bus-name aws.partner/shopify.com/12345/default \\\n --state ENABLED\n```\n\n**Example Event:**\n```json\n{\n \"detail-type\": \"orders/created\",\n \"detail\": {\n \"id\": \"gid://shopify/Order/123456\",\n \"displayOrderNumber\": \"#1001\",\n \"email\": \"customer@example.com\",\n \"createdAt\": \"2026-05-04T14:30:00Z\",\n \"totalPriceSet\": {\n \"shopMoney\": {\n \"amount\": \"299.99\",\n \"currencyCode\": \"USD\"\n }\n }\n },\n \"source\": \"aws.partner/shopify.com/12345/default\"\n}\n```\n\n### 3. Google Cloud Pub/Sub\n\nEvent streaming via Google Pub/Sub. Decoupled, scalable webhook delivery.\n\n**Characteristics:**\n- No public endpoint needed\n- Managed queue with exactly-once semantics\n- Scalable message processing\n- Requires Google Cloud project setup\n- Integration with Cloud Functions, Dataflow, etc.\n- Best for data pipeline workflows\n\n**Configuration:**\n```bash\n# 1. Create Pub/Sub topic in Google Cloud\ngcloud pubsub topics create shopify-webhooks\n\n# 2. Subscribe your app to receive messages\ngcloud pubsub subscriptions create shopify-webhook-sub \\\n --topic=shopify-webhooks \\\n --push-endpoint=https://your-service.com/pubsub-handler\n\n# 3. Create service account for Shopify to publish\ngcloud iam service-accounts create shopify-publisher\ngcloud pubsub topics add-iam-policy-binding shopify-webhooks \\\n --member=serviceAccount:shopify-publisher@PROJECT_ID.iam.gserviceaccount.com \\\n --role=roles/pubsub.publisher\n```\n\n## HMAC Verification (HTTPS Only)\n\nAll HTTPS webhooks are signed with HMAC-SHA256. Always verify signatures.\n\n**Headers:**\n- `X-Shopify-Hmac-SHA256`: Base64-encoded HMAC-SHA256 signature\n- `X-Shopify-Shop-Api-Access-Token`: OAuth token used (for debugging)\n- `X-Shopify-Webhook-Id`: Unique webhook instance ID\n- `X-Shopify-Topic`: Event topic (e.g., \"orders/created\")\n- `X-Shopify-Transmitted-At`: ISO 8601 timestamp\n\n**Verification Algorithm:**\n\n```javascript\n// Node.js verification\nconst crypto = require('crypto');\n\nfunction verifyWebhookSignature(req, secret) {\n const hmacHeader = req.headers['x-shopify-hmac-sha256'];\n const body = req.rawBody; // Must be raw bytes, not parsed JSON\n\n // Compute expected HMAC\n const computed = crypto\n .createHmac('sha256', secret)\n .update(body, 'utf8')\n .digest('base64');\n\n // Constant-time comparison\n return crypto.timingSafeEqual(\n Buffer.from(hmacHeader),\n Buffer.from(computed)\n );\n}\n\n// Express middleware example\napp.use(express.raw({ type: 'application/json' }));\n\napp.post('/webhooks/shopify', (req, res) => {\n if (!verifyWebhookSignature(req, process.env.SHOPIFY_WEBHOOK_SECRET)) {\n return res.status(401).send('Unauthorized');\n }\n\n const event = JSON.parse(req.body);\n // Process webhook...\n res.status(200).send('OK');\n});\n```\n\n**Python Verification:**\n\n```python\nimport hmac\nimport hashlib\nimport base64\n\ndef verify_webhook_signature(request, secret):\n hmac_header = request.headers.get('X-Shopify-Hmac-SHA256')\n body = request.get_data() # Raw bytes\n\n computed = base64.b64encode(\n hmac.new(\n secret.encode('utf-8'),\n body,\n hashlib.sha256\n ).digest()\n ).decode('utf-8')\n\n return hmac.compare_digest(hmac_header, computed)\n\nfrom flask import Flask, request\n\n@app.route('/webhooks/shopify', methods=['POST'])\ndef handle_webhook():\n if not verify_webhook_signature(request, SHOPIFY_WEBHOOK_SECRET):\n return 'Unauthorized', 401\n\n event = request.get_json()\n # Process webhook...\n return 'OK', 200\n```\n\n## Webhook Events and Payloads\n\n### Order Events\n\n**orders/created** - New order placed\n```graphql\nsubscription {\n event {\n id\n occurredAt\n ... on OrderCreatedEvent {\n order {\n id\n displayOrderNumber\n email\n totalPriceSet { shopMoney { amount currencyCode } }\n lineItems(first: 10) {\n edges {\n node {\n id\n title\n quantity\n variantId\n }\n }\n }\n }\n }\n }\n}\n```\n\n**orders/updated** - Order modified\n```graphql\nsubscription {\n event {\n ... on OrderUpdatedEvent {\n order {\n id\n status\n fulfillmentStatus\n tags\n }\n }\n }\n}\n```\n\n**orders/cancelled** - Order cancellation\n```graphql\nsubscription {\n event {\n ... on OrderCancelledEvent {\n order {\n id\n cancelReason\n cancelledAt\n }\n }\n }\n}\n```\n\n### Product Events\n\n**products/create** - New product\n**products/update** - Product modified\n**products/delete** - Product deleted\n\n### Customer Events\n\n**customers/create** - New customer account\n**customers/update** - Customer data changed\n**customers/delete** - Customer deleted (GDPR)\n\n### Fulfillment Events\n\n**fulfillments/created** - Items shipped\n**fulfillments/updated** - Fulfillment status changed\n**fulfillment_orders/scheduled** - Order ready to ship\n\n### GDPR Events\n\n**shop/redact** - Shop deletion requested\n**customers/redact** - Customer data erasure\n**orders/redact** - Order redaction (72-hour compliance)\n\n## Webhook Manifest Configuration\n\nModern apps define webhooks in `shopify.app.toml`:\n\n```toml\nscopes = \"write_orders,read_products\"\n\nwebhooks = {\n orders_create = {\n uri = \"api/webhooks/orders-create\"\n filter_query = \"query { event { id occurredAt } }\"\n }\n orders_update = {\n uri = \"api/webhooks/orders-update\"\n }\n products_create = {\n uri = \"api/webhooks/products-create\"\n }\n fulfillments_create = {\n uri = \"api/webhooks/fulfillments-create\"\n }\n customers_redact = {\n uri = \"api/webhooks/gdpr/customers-redact\"\n }\n orders_redact = {\n uri = \"api/webhooks/gdpr/orders-redact\"\n }\n shop_redact = {\n uri = \"api/webhooks/gdpr/shop-redact\"\n }\n}\n```\n\n## Retry Behavior\n\nHTTPS webhooks use exponential backoff:\n\n| Attempt | Delay | Total Time |\n|---------|-------|-----------|\n| 1 | Immediate | 0s |\n| 2 | 10 seconds | 10s |\n| 3 | 30 seconds | 40s |\n| 4 | 1 minute | 1m 40s |\n| 5 | 2 minutes | 3m 40s |\n| 6 | 8 hours | 8h 3m 40s |\n\n**Retry Conditions:**\n- HTTP 5xx errors: always retry\n- HTTP 4xx errors: no retry (except 429)\n- 429 Too Many Requests: respect Retry-After header, retry\n- Connection timeout: retry\n- SSL/TLS errors: no retry (fix cert, reregister)\n- Response timeout (30s): retry\n\n**Idempotent Processing Pattern:**\n\n```javascript\nconst db = require('./database');\n\napp.post('/webhooks/shopify', async (req, res) => {\n if (!verifyWebhookSignature(req, SECRET)) {\n return res.status(401).send('Unauthorized');\n }\n\n const webhookId = req.headers['x-shopify-webhook-id'];\n const event = JSON.parse(req.body);\n\n // Check if already processed\n const existing = await db.webhookLog.findOne({ webhookId });\n if (existing) {\n return res.status(200).send('Already processed');\n }\n\n try {\n // Process event\n if (event.id.includes('Order')) {\n await handleOrderEvent(event);\n }\n\n // Record successful processing\n await db.webhookLog.create({\n webhookId,\n topic: req.headers['x-shopify-topic'],\n processedAt: new Date(),\n status: 'success'\n });\n\n res.status(200).send('OK');\n } catch (error) {\n // Log error, let retry happen\n console.error('Webhook processing failed:', error);\n res.status(500).send('Processing error');\n }\n});\n```\n\n## GDPR Compliance Webhooks\n\nHandle data erasure requests within 30 days.\n\n**Customer Redaction (24-hour notice):**\n```graphql\nsubscription {\n event {\n ... on CustomerRedactEvent {\n customerId\n ordersToRedact\n }\n }\n}\n```\n\nHandler implementation:\n```javascript\napp.post('/webhooks/gdpr/customer-redact', async (req, res) => {\n const { customerId, ordersToRedact } = req.body;\n\n // Delete all customer data\n await db.customers.deleteOne({ shopifyId: customerId });\n await db.orders.updateMany(\n { _id: { $in: ordersToRedact } },\n { $unset: { customerEmail: '', customerPhone: '' } }\n );\n\n res.status(200).send('Redacted');\n});\n```\n\n**Shop Redaction (48-hour notice):**\n```javascript\napp.post('/webhooks/gdpr/shop-redact', async (req, res) => {\n const { shopId } = req.body;\n\n // Delete all shop and customer data\n await db.shops.deleteOne({ shopifyId: shopId });\n await db.customers.deleteMany({ shopifyId });\n\n res.status(200).send('Shop deleted');\n});\n```\n\n## Implementation Patterns\n\n### Remix Framework Pattern\n\n```typescript\n// app/routes/webhooks/shopify.tsx\nimport { json, type ActionFunction } from '@remix-run/node';\nimport crypto from 'crypto';\n\nfunction verifyWebhookSignature(\n request: Request,\n secret: string\n): boolean {\n const hmacHeader = request.headers.get('x-shopify-hmac-sha256');\n const body = request.body;\n\n const computed = crypto\n .createHmac('sha256', secret)\n .update(body, 'utf8')\n .digest('base64');\n\n return crypto.timingSafeEqual(\n Buffer.from(hmacHeader || ''),\n Buffer.from(computed)\n );\n}\n\nexport const action: ActionFunction = async ({ request }) => {\n if (request.method !== 'POST') {\n return json({ error: 'Method not allowed' }, { status: 405 });\n }\n\n if (!verifyWebhookSignature(request, process.env.WEBHOOK_SECRET!)) {\n return json({ error: 'Unauthorized' }, { status: 401 });\n }\n\n const event = await request.json();\n\n switch (request.headers.get('x-shopify-topic')) {\n case 'orders/created':\n await handleOrderCreated(event);\n break;\n case 'products/updated':\n await handleProductUpdated(event);\n break;\n case 'customers/redact':\n await handleCustomerRedact(event);\n break;\n }\n\n return json({ success: true });\n};\n```\n\n### Next.js API Route Pattern\n\n```typescript\n// pages/api/webhooks/shopify.ts\nimport { NextApiRequest, NextApiResponse } from 'next';\nimport crypto from 'crypto';\n\nfunction verifySignature(req: NextApiRequest, secret: string): boolean {\n const signature = req.headers['x-shopify-hmac-sha256'] as string;\n const body = (req as any).rawBody;\n\n const hash = crypto\n .createHmac('sha256', secret)\n .update(body)\n .digest('base64');\n\n return crypto.timingSafeEqual(\n Buffer.from(signature),\n Buffer.from(hash)\n );\n}\n\n// Middleware to capture raw body\nexport const config = {\n api: {\n bodyParser: false,\n },\n};\n\nexport default async function handler(\n req: NextApiRequest,\n res: NextApiResponse\n) {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' });\n }\n\n // Capture raw body\n const chunks: Buffer[] = [];\n for await (const chunk of req) {\n chunks.push(chunk);\n }\n (req as any).rawBody = Buffer.concat(chunks).toString('utf-8');\n\n if (!verifySignature(req, process.env.WEBHOOK_SECRET!)) {\n return res.status(401).json({ error: 'Unauthorized' });\n }\n\n const body = JSON.parse((req as any).rawBody);\n\n // Process webhook\n await processWebhook(req.headers['x-shopify-topic'] as string, body);\n\n res.status(200).json({ success: true });\n}\n```\n\n## Webhook Payload Examples\n\n**Order Created Payload:**\n```json\n{\n \"id\": 1234567890,\n \"email\": \"customer@example.com\",\n \"display_order_number\": \"#1001\",\n \"created_at\": \"2026-05-04T14:30:00Z\",\n \"updated_at\": \"2026-05-04T14:30:00Z\",\n \"total_price\": \"299.99\",\n \"currency\": \"USD\",\n \"line_items\": [\n {\n \"id\": 9876543210,\n \"title\": \"Blue T-Shirt\",\n \"variant_id\": 1111111111,\n \"quantity\": 2,\n \"price\": \"29.99\"\n }\n ],\n \"customer\": {\n \"id\": 5555555555,\n \"email\": \"customer@example.com\",\n \"first_name\": \"John\",\n \"last_name\": \"Doe\"\n }\n}\n```\n\n**Product Updated Payload:**\n```json\n{\n \"id\": 1234567890,\n \"title\": \"Blue T-Shirt\",\n \"handle\": \"blue-t-shirt\",\n \"vendor\": \"Example Vendor\",\n \"product_type\": \"Apparel\",\n \"created_at\": \"2026-01-01T00:00:00Z\",\n \"updated_at\": \"2026-05-04T14:30:00Z\",\n \"variants\": [\n {\n \"id\": 1111111111,\n \"title\": \"Small / Blue\",\n \"sku\": \"BTS-S-BLU\",\n \"price\": \"29.99\",\n \"inventory_quantity\": 50\n }\n ]\n}\n```\n\n## Common Gotchas\n\n| Issue | Cause | Solution |\n|-------|-------|----------|\n| Invalid signature | Raw body passed to parser | Capture raw body before JSON parsing |\n| Duplicate processing | Retries without idempotency | Store webhook IDs, check before processing |\n| Timeout errors | Slow processing in handler | Process asynchronously, return 200 immediately |\n| Missing events | Webhook deregistered | Check app installation, reauth if needed |\n| GDPR violation | Not respecting 30-day deadline | Implement automated redaction workflow |\n| Rate limit rejection | Too many API calls in handler | Batch requests, use bulk operations |\n| SSL certificate errors | Expired or invalid cert | Renew cert, restart webhook delivery |\n| Event data incomplete | Querying without proper fields | Subscribe with full field selections |\n| Webhook loop | Webhook triggers same event | Add idempotency guard, check source app |\n| Lost messages | HTTPS retry limit exceeded | Implement event queue, use EventBridge/Pub/Sub |\n\n## Decision Tree: Choosing Delivery Method\n\n```\nSTART: Do you need real-time delivery?\n├─ YES → Can you expose public HTTPS endpoint?\n│ ├─ YES → Use HTTPS (traditional, simplest)\n│ └─ NO → Go to AWS/GCP check\n├─ NO → Use EventBridge/Pub/Sub (async)\n └─ Do you use AWS?\n ├─ YES → Use EventBridge\n └─ NO → Use Google Pub/Sub\n```\n\n## Best Practices\n\n1. **Always verify signatures** - Never skip HMAC verification\n2. **Process asynchronously** - Use queues, return 200 immediately\n3. **Implement idempotency** - Store webhook IDs, deduplicate\n4. **Handle retries gracefully** - Exponential backoff already applied\n5. **Monitor webhook health** - Track delivery success rates\n6. **Log all events** - For debugging and audit trails\n7. **Use webhooks manifest** - Declarative, cleaner than APIs\n8. **Respect rate limits** - Don't make too many API calls in handlers\n9. **GDPR compliance** - Process redaction webhooks within 30 days\n10. **Test locally** - Use ngrok or Shopify CLI to tunnel webhooks\n"
}SHA-256 of public snapshot: 2b559d1cc916e8caf1011af46bf6290b0a05d38e79d5c6daaac399533ee21e6b