# Guide to Webflow Webhooks: Features and Best Practices

Webflow webhooks notify your systems when something happens on your site: a form submission comes in, a CMS item changes, an ecommerce order lands, the site is published. If you're connecting Webflow to a CRM, a fulfillment system, or a build pipeline, webhooks are how those systems find out in real time instead of polling.

This guide covers how Webflow webhooks work, the two ways to create them (and why only one gives you signature headers), the HMAC-SHA256 verification scheme, the platform's delivery limits, and the best practices for production.

## What are Webflow webhooks?

Webflow webhooks are HTTP callbacks that notify your application when events occur on a Webflow site. When a subscribed event fires, Webflow POSTs a JSON payload to your endpoint. Every event follows a consistent envelope: a `triggerType` field naming the event, and a `payload` object carrying the event-specific data, such as the submitted form fields, the order details, or the CMS item that changed.

## Webflow webhook features

| Feature | Details |
| --- | --- |
| Configuration | Dashboard (Project Settings > Integrations > Webhooks) or API (`POST /sites/{site_id}/webhooks`) |
| Authentication | HMAC-SHA256 (hex) over `timestamp:body` in `x-webflow-signature`, with `x-webflow-timestamp`; only on OAuth-app or API-created webhooks |
| Envelope | `{ "triggerType": ..., "payload": ... }`, with event data nested under `payload` |
| Payload size | Up to 256 KB |
| Timeout | 30 seconds per request |
| Retries | Up to 3 attempts on failure, at 10-minute intervals |
| Limits | Up to 75 webhooks per trigger type |
| Scopes | API creation requires the matching scope per event family (e.g. `forms:read` for `form_submission`, `ecommerce:read` for `ecomm_*`) |

## Common events

Webflow event names are the `triggerType` values:

| Event | Fires when |
| --- | --- |
| `form_submission` | A form is submitted on your site |
| `site_publish` | The site is published |
| `page_created` | A new page is created |
| `page_deleted` | A page is deleted |
| `ecomm_new_order` | A new ecommerce order is placed |
| `ecomm_order_changed` | Order status or details change |
| `ecomm_inventory_changed` | Product inventory changes |
| `user_account_added` | A new user account is created |
| `collection_item_created` | A CMS item is created |
| `collection_item_changed` | A CMS item is updated |
| `collection_item_deleted` | A CMS item is deleted |
| `collection_item_unpublished` | A CMS item is unpublished |

Branch on the `triggerType` field at the top level of the body; the event-specific data lives under `payload`, and its shape varies by event.

> See Webflow webhook payloads in action. Inspect and replay sample Webflow webhook payloads in the [Hookdeck Console](https://console.hookdeck.com) — no account or setup required.

## Setting up Webflow webhooks

There are two ways to create a webhook, and they differ in more than convenience: only webhooks created via an OAuth app or the API include signature headers.

In the dashboard, go to Project Settings > Integrations > Webhooks, click Add Webhook, select the trigger event, enter your endpoint URL, and save. This is the quickest path, but deliveries arrive with no signature headers, so there is nothing to verify.

Via the API, POST to the webhooks endpoint with a trigger type and URL (an optional `filter` narrows form webhooks to a named form):

```bash
curl -X POST https://api.webflow.com/sites/{site_id}/webhooks \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "triggerType": "form_submission",
    "url": "https://your-app.com/webhooks/webflow"
  }'

```

For webhooks created after April 2025, the creation response includes a `secret` field (`whsec_...`): save it, as it is your signing secret for that webhook. For webhooks created through an OAuth app, the app's client secret is the signing secret instead. Each trigger type requires the matching API scope (`forms:read` for `form_submission`, `cms:read` for `collection_item_*`, `ecommerce:read` for `ecomm_*` events, and so on), and the same API surface lets you list (`GET`), update (`PATCH`), and delete (`DELETE`) webhooks.

When testing, remember that the site must be published: draft changes do not trigger webhooks.

For local development, use the [Hookdeck CLI](/docs/cli): `hookdeck listen 3000 webflow --path /webhooks/webflow` gives you a public HTTPS URL that forwards to your local server, plus a web UI for inspecting and replaying deliveries, with no account required. Register the generated URL as your webhook endpoint.

## Securing Webflow webhooks

Signed deliveries (OAuth-app or API-created webhooks) carry two headers: `x-webflow-timestamp`, a Unix epoch timestamp in milliseconds, and `x-webflow-signature`, a hex-encoded HMAC-SHA256 hash. Webflow concatenates the timestamp and the raw request body with a colon (`timestamp:body`) and signs that string with your secret.

Verification has three requirements. First, use the raw request body, not parsed JSON; in Express that means `express.raw()` on the route. Second, validate the timestamp against a 5-minute window (300000 milliseconds) to prevent replay attacks. Third, compare signatures with a timing-safe comparison. Access the headers in lowercase, since some frameworks normalize header names:

```javascript
const crypto = require("crypto");

function verifyWebflowSignature(rawBody, signature, timestamp, secret) {
  // x-webflow-timestamp is a Unix epoch timestamp in milliseconds;
  // reject anything outside the 5-minute replay window
  const timeDiff = Math.abs(Date.now() - parseInt(timestamp, 10));
  if (isNaN(timeDiff) || timeDiff > 300000) {
    return false;
  }

  const expectedSignature = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}:${rawBody}`)
    .digest("hex");

  try {
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignature)
    );
  } catch {
    return false; // Buffers of different lengths throw
  }
}

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 required 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);

  switch (event.triggerType) {
    case "form_submission":
      // Lead capture, CRM sync
      break;
    case "ecomm_new_order":
      // Order processing, fulfillment
      break;
  }

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

```

Note the encoding: Webflow signatures are hex, so `.digest("hex")`, not base64.

> Make Webflow webhooks production-ready. [Hookdeck Event Gateway](/event-gateway) verifies deliveries at the edge, deduplicates, and durably queues every event with replay for anything that fails.

## Webflow webhook limitations and pain points

### Dashboard-created webhooks cannot be verified

The Problem: Webhooks created through the Webflow dashboard arrive with no `x-webflow-signature` or `x-webflow-timestamp` headers, so there is no way to confirm a delivery actually came from Webflow. Anyone who learns the URL can POST fabricated form submissions or orders.

Why It Happens: Only webhooks created via OAuth apps or the API include signature headers; the dashboard path omits them entirely.

Workarounds:

* Create production webhooks via the API (or an OAuth app) so deliveries are signed.
* Recreate any existing dashboard webhooks through the API; a "Missing required headers" failure in your handler is the telltale sign of a dashboard-created webhook.
* Treat any unsigned endpoint as untrusted input and validate the payload before acting on it.

How Hookdeck Can Help: Hookdeck gives you a dedicated ingestion URL and can verify the HMAC signature at the edge for signed webhooks, so unverifiable traffic never reaches your handler, and every delivery is logged with full headers and body for inspection.

### Three retries, then the event is gone

The Problem: On a failed delivery, Webflow retries up to 3 times at 10-minute intervals. An outage longer than that retry window means missed events, and there is no mechanism to fetch them afterwards.

Why It Happens: The retry policy is fixed: 3 attempts, 10 minutes apart, with a 30-second timeout per request.

Workarounds:

* Return 200 immediately and defer processing, so slow work never turns into a failed delivery.
* Monitor your endpoint's error rate and fix failures fast; the recovery window is short.
* Log raw payloads on receipt so you at least have a record of what arrived before a processing bug.

How Hookdeck Can Help: Hookdeck ingests and durably queues events ahead of your endpoint, with [automatic retries](/docs/retries) on your delivery schedule, [Issues](/docs/issues) that alert you when deliveries fail, and replay for any event, so a 30-minute outage in your infrastructure no longer means data loss.

### Two different signing secrets

The Problem: The secret you verify with depends on how the webhook was created. OAuth-app webhooks sign with the app's client secret; API-created webhooks (after April 2025) sign with a webhook-specific `whsec_` secret returned once at creation. Use the wrong one and every delivery fails verification with the same unhelpful result: an invalid signature.

Why It Happens: Webflow moved API-created webhooks to webhook-specific secrets in April 2025, while OAuth-app webhooks kept the client-secret model, so two schemes coexist.

Workarounds:

* Know your creation path: OAuth app means client secret, API-created means the `whsec_` value from the creation response.
* Save the `secret` field when you create a webhook via the API; it is your only chance to capture it from the response.
* When debugging, log the first few characters of the secret in use alongside the computed and received signatures to spot mismatches quickly.

How Hookdeck Can Help: Every delivery is logged with its full headers and body, so you can inspect exactly what was signed and debug verification against real requests instead of guessing from a bare "Invalid signature" in production logs.

### One trigger type per webhook

The Problem: Each webhook subscribes to a single `triggerType`. Covering forms, publishing, ecommerce, and the CMS means creating and maintaining a separate webhook registration per event type, each requiring the matching API scope.

Why It Happens: Webflow's model registers URL and trigger type pairs, capped at 75 webhooks per trigger type, with scopes granted per event family.

Workarounds:

* Point every trigger type at the same endpoint and branch on `triggerType` in one handler.
* Script webhook creation via the API so registrations are reproducible across sites and environments.
* Use the list endpoint to audit what is registered and prune stale entries.

How Hookdeck Can Help: Register a single Hookdeck source URL for all your trigger types, then use [filters](/docs/filters) to route events by `triggerType` to different destinations, so fan-out lives in configuration rather than in a growing switch statement.

## Best practices

### Create webhooks through the API

Dashboard-created webhooks cannot be verified. Create production webhooks via the API or an OAuth app so every delivery carries signature headers, and store the signing secret in an environment variable.

### Verify with the raw body and the timestamp window

Signature verification requires the raw request body, hex encoding, and a timestamp check against the 5-minute window, compared with a timing-safe function. Parsed-then-reserialized JSON is the most common cause of verification failures.

### Acknowledge fast, process asynchronously

Webflow times out requests after 30 seconds and retries failures only 3 times. Return 200 as soon as the signature checks out and hand the event to a queue or background job. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Make handlers idempotent

Failed or slow responses trigger retries, so the same event can arrive more than once. Make processing safe to repeat so a retried `ecomm_new_order` never becomes a double fulfillment. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Publish to test

Draft changes do not trigger webhooks. To exercise an integration end to end, publish the site, then submit the form or update the CMS item, and check your endpoint logs.

## Conclusion

Webflow webhooks cover the events that matter on a site (forms, publishing, ecommerce, user accounts, and the CMS) in a consistent `triggerType` plus `payload` envelope. The decisions that shape a production integration happen at creation time: webhooks made through the API are signed with HMAC-SHA256 over `timestamp:body` and can be verified; webhooks made in the dashboard cannot.

The delivery guarantees are modest, at 3 retries 10 minutes apart, so an endpoint that acknowledges fast and processes asynchronously is the difference between a resilient integration and silent data loss. [Hookdeck Event Gateway](/event-gateway) puts verification, deduplication, and a durable queue with replay in front of your endpoint, so your handlers only ever process trustworthy events.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Webflow webhooks reliably in minutes.