{"id":17079,"plugin_id":"plugins_6a701c7b1f9481919cf7c7448ddc1bd4","kind":"skill","collection_source":null,"comparison_source":null,"observed_at":"2026-09-30T23:13:56.770Z","digest":"2b559d1cc916e8caf1011af46bf6290b0a05d38e79d5c6daaac399533ee21e6b","against":null,"payload":{"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"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}