$npx -y skills add hookdeck/webhook-skills --skill webflow-webhooksReceive and verify Webflow webhooks. Use when setting up Webflow webhook handlers, debugging signature verification, or handling Webflow events like form_submission, site_publish, ecomm_new_order, or collection item changes.
| 1 | # Webflow Webhooks |
| 2 | |
| 3 | ## When to Use This Skill |
| 4 | |
| 5 | - How do I receive Webflow webhooks? |
| 6 | - How do I verify Webflow webhook signatures? |
| 7 | - How do I handle form_submission events from Webflow? |
| 8 | - How do I process Webflow ecommerce order events? |
| 9 | - Why is my Webflow webhook signature verification failing? |
| 10 | - Setting up Webflow CMS collection item webhooks |
| 11 | |
| 12 | ## Essential Code |
| 13 | |
| 14 | ### Signature Verification (Manual) |
| 15 | |
| 16 | ```javascript |
| 17 | const crypto = require('crypto'); |
| 18 | |
| 19 | function verifyWebflowSignature(rawBody, signature, timestamp, secret) { |
| 20 | // Check timestamp to prevent replay attacks (5 minute window - 300000 milliseconds) |
| 21 | const currentTime = Date.now(); |
| 22 | if (Math.abs(currentTime - parseInt(timestamp)) > 300000) { |
| 23 | return false; |
| 24 | } |
| 25 | |
| 26 | // Generate HMAC signature |
| 27 | const signedContent = `${timestamp}:${rawBody}`; |
| 28 | const expectedSignature = crypto |
| 29 | .createHmac('sha256', secret) |
| 30 | .update(signedContent) |
| 31 | .digest('hex'); |
| 32 | |
| 33 | // Timing-safe comparison |
| 34 | try { |
| 35 | return crypto.timingSafeEqual( |
| 36 | Buffer.from(signature), |
| 37 | Buffer.from(expectedSignature) |
| 38 | ); |
| 39 | } catch { |
| 40 | return false; // Different lengths = invalid |
| 41 | } |
| 42 | } |
| 43 | ``` |
| 44 | |
| 45 | ### Processing Events |
| 46 | |
| 47 | ```javascript |
| 48 | app.post('/webhooks/webflow', express.raw({ type: 'application/json' }), (req, res) => { |
| 49 | const signature = req.headers['x-webflow-signature']; |
| 50 | const timestamp = req.headers['x-webflow-timestamp']; |
| 51 | |
| 52 | if (!signature || !timestamp) { |
| 53 | return res.status(400).send('Missing required headers'); |
| 54 | } |
| 55 | |
| 56 | // Verify signature (use OAuth client secret or webhook-specific secret) |
| 57 | const isValid = verifyWebflowSignature( |
| 58 | req.body.toString(), |
| 59 | signature, |
| 60 | timestamp, |
| 61 | process.env.WEBFLOW_WEBHOOK_SECRET |
| 62 | ); |
| 63 | |
| 64 | if (!isValid) { |
| 65 | return res.status(400).send('Invalid signature'); |
| 66 | } |
| 67 | |
| 68 | // Parse the verified payload |
| 69 | const event = JSON.parse(req.body); |
| 70 | |
| 71 | // Handle different event types |
| 72 | switch (event.triggerType) { |
| 73 | case 'form_submission': |
| 74 | console.log('New form submission:', event.payload.data); |
| 75 | break; |
| 76 | case 'ecomm_new_order': |
| 77 | console.log('New order:', event.payload); |
| 78 | break; |
| 79 | case 'collection_item_created': |
| 80 | console.log('New CMS item:', event.payload); |
| 81 | break; |
| 82 | // Add more event handlers as needed |
| 83 | } |
| 84 | |
| 85 | // Always return 200 to acknowledge receipt |
| 86 | res.status(200).send('OK'); |
| 87 | }); |
| 88 | ``` |
| 89 | |
| 90 | ## Common Event Types |
| 91 | |
| 92 | | Event | Triggered When | Use Case | |
| 93 | |-------|----------------|----------| |
| 94 | | `form_submission` | Form submitted on site | Contact forms, lead capture | |
| 95 | | `site_publish` | Site is published | Clear caches, trigger builds | |
| 96 | | `ecomm_new_order` | New ecommerce order | Order processing, inventory | |
| 97 | | `ecomm_order_changed` | Order status changes | Update fulfillment systems | |
| 98 | | `collection_item_created` | CMS item created | Content syndication | |
| 99 | | `collection_item_changed` | CMS item updated | Update external systems | |
| 100 | | `collection_item_deleted` | CMS item deleted | Remove from external systems | |
| 101 | |
| 102 | ## Environment Variables |
| 103 | |
| 104 | ```bash |
| 105 | # For webhooks created via OAuth App |
| 106 | WEBFLOW_WEBHOOK_SECRET=your_oauth_client_secret |
| 107 | |
| 108 | # For webhooks created via API (after April 2025) |
| 109 | WEBFLOW_WEBHOOK_SECRET=whsec_xxxxx # Returned when creating webhook |
| 110 | ``` |
| 111 | |
| 112 | ## Local Development |
| 113 | |
| 114 | For local webhook testing, install Hookdeck CLI: |
| 115 | |
| 116 | ```bash |
| 117 | ``` |
| 118 | |
| 119 | Then start the tunnel: |
| 120 | |
| 121 | ```bash |
| 122 | npx hookdeck-cli listen 3000 webflow --path /webhooks/webflow |
| 123 | ``` |
| 124 | |
| 125 | No account required. Provides local tunnel + web UI for inspecting requests. |
| 126 | |
| 127 | ## Resources |
| 128 | |
| 129 | - [What Are Webflow Webhooks](references/overview.md) - Event types and payload structure |
| 130 | - [Setting Up Webflow Webhooks](references/setup.md) - Dashboard configuration and API setup |
| 131 | - [Signature Verification Details](references/verification.md) - In-depth verification guide |
| 132 | - [Express Example](examples/express/) - Node.js implementation with tests |
| 133 | - [Next.js Example](examples/nextjs/) - App Router implementation |
| 134 | - [FastAPI Example](examples/fastapi/) - Python implementation |
| 135 | |
| 136 | ## Important Notes |
| 137 | |
| 138 | - Webhooks created through the Webflow dashboard do NOT include signature headers |
| 139 | - Only webhooks created via OAuth apps or API include `x-webflow-signature` and `x-webflow-timestamp` |
| 140 | - Always use raw body for signature verification, not parsed JSON |
| 141 | - Timestamp validation (5 minute window - 300000 milliseconds) is critical to prevent replay attacks |
| 142 | - Return 200 status to acknowledge receipt; other statuses trigger retries (up to 3 times) |
| 143 | |
| 144 | ## Recommended: webhook-handler-patterns |
| 145 | |
| 146 | This skill pairs well with webhook-handler-patterns for production-ready implementations: |
| 147 | |
| 148 | - [Handler sequence](https://github.com/hookdeck/web |