Skip to main content

Overview

Webflow Webhooks enable real-time event notifications for form submissions, CMS changes, ecommerce orders, site publishing, and more. Webhooks deliver HTTP POST requests to your endpoint whenever specific events occur in Webflow.
Webhooks created via API or OAuth include signature verification. Dashboard webhooks do not include signatures.

Download Skill

Add this skill to your AI agent for webhook integration assistance:
https://skills.224ai.au/webflow-webhooks.skill

Quick Start

1

Create webhook

Register a webhook via the Webflow API for your desired event type:
POST https://api.webflow.com/v2/sites/{site_id}/webhooks
2

Set up endpoint

Create an endpoint that accepts POST requests with raw body parsing:
app.post('/webhooks/webflow', express.raw({ type: 'application/json' }), handler);
3

Verify signatures

Validate x-webflow-signature and x-webflow-timestamp headers using HMAC-SHA256
4

Process events

Route events by triggerType and handle each accordingly
5

Acknowledge receipt

Return 200 status to confirm receipt (other statuses trigger retries)

Event Types

Webflow supports 14 webhook event types across 6 categories:
CategoryEventsRequired Scope
Formsform_submissionforms:read
Sitesite_publishsites:read
Pagespage_created, page_metadata_updated, page_deletedpages:read
Ecommerceecomm_new_order, ecomm_order_changed, ecomm_inventory_changedecommerce:read
CMScollection_item_created, collection_item_changed, collection_item_deleted, collection_item_unpublished, collection_item_publishedcms:read
Commentscomment_createdcomments:read
See references/event-types.md for complete payload schemas and examples for all 14 event types.

Signature Verification

Webhooks created via API or OAuth include cryptographic signatures for verification.

Verification Code

const crypto = require('crypto');

function verifyWebflowSignature(rawBody, signature, timestamp, secret) {
  // Check timestamp to prevent replay attacks (5 minute window)
  const currentTime = Date.now();
  if (Math.abs(currentTime - parseInt(timestamp)) > 300000) {
    return false;
  }

  // Generate HMAC signature
  const signedContent = `${timestamp}:${rawBody}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedContent)
    .digest('hex');

  // Timing-safe comparison
  try {
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignature)
    );
  } catch {
    return false; // Different lengths = invalid
  }
}
Always use raw body for verification. Never verify against parsed JSON. Configure your framework accordingly.

Framework-Specific Setup

const express = require('express');
const app = express();

// Use express.raw() for webhook endpoint
app.post('/webhooks/webflow', 
  express.raw({ type: 'application/json' }), 
  (req, res) => {
    const signature = req.headers['x-webflow-signature'];
    const timestamp = req.headers['x-webflow-timestamp'];
    
    if (!signature || !timestamp) {
      return res.status(400).send('Missing headers');
    }

    const isValid = verifyWebflowSignature(
      req.body.toString(),
      signature,
      timestamp,
      process.env.WEBFLOW_WEBHOOK_SECRET
    );

    if (!isValid) {
      return res.status(400).send('Invalid signature');
    }

    const event = JSON.parse(req.body);
    // Process event...
    
    res.status(200).send('OK');
  }
);
// pages/api/webhooks/webflow.ts
import { NextApiRequest, NextApiResponse } from 'next';
import crypto from 'crypto';

export const config = {
  api: {
    bodyParser: false, // Disable body parsing
  },
};

async function getRawBody(req: NextApiRequest): Promise<string> {
  const chunks = [];
  for await (const chunk of req) {
    chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
  }
  return Buffer.concat(chunks).toString('utf8');
}

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const signature = req.headers['x-webflow-signature'] as string;
  const timestamp = req.headers['x-webflow-timestamp'] as string;

  if (!signature || !timestamp) {
    return res.status(400).json({ error: 'Missing headers' });
  }

  const rawBody = await getRawBody(req);
  const isValid = verifyWebflowSignature(
    rawBody,
    signature,
    timestamp,
    process.env.WEBFLOW_WEBHOOK_SECRET!
  );

  if (!isValid) {
    return res.status(400).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(rawBody);
  // Process event...

  res.status(200).json({ success: true });
}
const fastify = require('fastify')();

fastify.post('/webhooks/webflow', {
  config: {
    rawBody: true
  }
}, async (request, reply) => {
  const signature = request.headers['x-webflow-signature'];
  const timestamp = request.headers['x-webflow-timestamp'];

  if (!signature || !timestamp) {
    return reply.code(400).send('Missing headers');
  }

  const isValid = verifyWebflowSignature(
    request.rawBody,
    signature,
    timestamp,
    process.env.WEBFLOW_WEBHOOK_SECRET
  );

  if (!isValid) {
    return reply.code(400).send('Invalid signature');
  }

  const event = JSON.parse(request.rawBody);
  // Process event...

  return reply.code(200).send('OK');
});

Processing Events

Route events by triggerType and handle each event appropriately:
app.post('/webhooks/webflow', express.raw({ type: 'application/json' }), (req, res) => {
  // Verify signature (see above)
  
  const event = JSON.parse(req.body);

  switch (event.triggerType) {
    case 'form_submission':
      console.log('New form submission:', event.payload.data);
      // Send to CRM, email notification, etc.
      break;

    case 'ecomm_new_order':
      console.log('New order:', event.payload.orderId);
      // Process fulfillment, send confirmation email
      break;

    case 'collection_item_created':
      console.log('New CMS item:', event.payload);
      // Sync to external database, trigger build
      break;

    case 'collection_item_published':
      console.log('Published CMS items:', event.payload.items);
      // Trigger static site regeneration
      break;

    case 'site_publish':
      console.log('Site published:', event.site);
      // Clear CDN cache, notify team
      break;

    default:
      console.log('Unhandled event:', event.triggerType);
  }

  res.status(200).send('OK');
});

Environment Variables

Never hard-code secrets. Always use environment variables or a secrets manager.
# For webhooks created via OAuth App
WEBFLOW_WEBHOOK_SECRET=your_oauth_client_secret

# For webhooks created via API (after April 2025)
WEBFLOW_WEBHOOK_SECRET=whsec_xxxxx  # Returned when creating webhook

Webhook Management API

Create, list, get, and delete webhooks programmatically:

Create a Webhook

const webhook = {
  triggerType: 'form_submission',
  url: 'https://your-domain.com/webhooks/webflow'
};

const response = await fetch(
  'https://api.webflow.com/v2/sites/YOUR_SITE_ID/webhooks',
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.WEBFLOW_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(webhook)
  }
);

const data = await response.json();
console.log('Webhook created:', data);

List All Webhooks

const response = await fetch(
  'https://api.webflow.com/v2/sites/YOUR_SITE_ID/webhooks',
  {
    headers: {
      'Authorization': `Bearer ${process.env.WEBFLOW_TOKEN}`,
      'Accept': 'application/json'
    }
  }
);

const data = await response.json();
data.webhooks.forEach(webhook => {
  console.log(`${webhook.triggerType}${webhook.url}`);
});

Delete a Webhook

const response = await fetch(
  'https://api.webflow.com/v2/webhooks/WEBHOOK_ID',
  {
    method: 'DELETE',
    headers: {
      'Authorization': `Bearer ${process.env.WEBFLOW_TOKEN}`
    }
  }
);

if (response.ok) {
  console.log('Webhook deleted successfully');
}

Best Practices

Always Verify Signatures

Use HMAC-SHA256 verification for webhooks created via OAuth or API

Use Raw Body

Never verify against parsed JSON — configure framework for raw body parsing

Validate Timestamps

Enforce 5-minute window (300000ms) to prevent replay attacks

Return 200 Quickly

Acknowledge receipt immediately; process events asynchronously

Handle Retries

Webflow retries up to 3 times on failure — implement idempotency

Use HTTPS

Webhook endpoints must use HTTPS in production for security

Retry Behavior

Webflow automatically retries failed webhook deliveries:
  • Retry count: Up to 3 attempts
  • Retry interval: 10 minutes between attempts
  • Success status: Only 200 status codes are considered successful
  • Timeout: 30 seconds per request
Implement idempotency in your webhook handler to safely process duplicate events from retries.
const processedEvents = new Set();

app.post('/webhooks/webflow', express.raw({ type: 'application/json' }), (req, res) => {
  const event = JSON.parse(req.body);
  
  // Check if already processed (simple idempotency)
  const eventId = `${event.triggerType}-${event._id}`;
  if (processedEvents.has(eventId)) {
    return res.status(200).send('Already processed');
  }
  
  // Process event
  processedEvents.add(eventId);
  // ... handle event ...
  
  res.status(200).send('OK');
});

Debugging

Common issues:
  • Using parsed JSON instead of raw body
  • Wrong secret (OAuth client secret vs webhook secret)
  • Timestamp out of 5-minute window
  • Missing or malformed headers
// Debug logging
console.log('Raw body:', req.body.toString());
console.log('Signature:', req.headers['x-webflow-signature']);
console.log('Timestamp:', req.headers['x-webflow-timestamp']);
console.log('Secret:', process.env.WEBFLOW_WEBHOOK_SECRET?.substring(0, 10) + '...');
Checklist:
  • Webhook URL is publicly accessible
  • HTTPS is enabled (required in production)
  • Firewall allows incoming requests
  • Endpoint returns 200 status
  • Correct event type registered
  • Required API scopes granted
  • Check event payload schema in references/event-types.md
  • Some events have nested data structures
  • Published items are arrays, not single objects
  • Form submissions store data in payload.data

Important Notes

  • Webhooks created through the Webflow dashboard do NOT include signature headers
  • Only webhooks created via OAuth apps or API include x-webflow-signature and x-webflow-timestamp
  • Timestamp validation (5 minute window) is critical to prevent replay attacks
  • Return 200 status to acknowledge receipt; other statuses trigger retries
  • Use HTTPS for production webhook endpoints

Reference Documentation

The skill includes comprehensive reference documentation:
  • event-types.md — Complete reference for all 14 event types with scopes, payload schemas, and examples
  • webhook-api.md — REST API v2 endpoints for creating, listing, getting, and deleting webhooks
  • overview.md — Webhook concepts, delivery behavior, limits, and security considerations
  • setup.md — Dashboard and API configuration, OAuth, scopes, environment setup
  • verification.md — HMAC-SHA256 signature verification, common gotchas, debugging
  • faq.md — FAQ and troubleshooting for delivery issues, signature failures, and API errors

Searching References

# List all references with metadata
python scripts/search_references.py --list

# Search by tag (exact match)
python scripts/search_references.py --tag verification

# Search by keyword
python scripts/search_references.py --search form_submission

License

MIT License - See the repository for details.

Build docs developers (and LLMs) love