Forms

This content was generated with the assistance of AI and should be verified against the official Webflow documentation before being relied on in production.

This sub-section’s Webflow bibliography lists the reference material consulted while preparing these pages.

This page describes Webflow forms as they were on 2026-10-28, the date on which their behaviour and limits were checked against the official documentation. Webflow has no version numbers, so everything below is dated rather than versioned; the Product Changes and Deprecations page tracks what changes afterwards. Plans are named by tier only (Starter, Basic, Premium, Team, Enterprise and the Ecommerce tiers) and no price is quoted; the Plans, Workspaces, Seats and Roles page explains what each tier includes.

Forms are the most common reason a marketing site needs a back end. A visitor types something, presses a button, and somebody on the team has to receive it, read it, answer it and, in some cases, keep it for a legal period. Webflow provides that back end as part of the hosting: the form posts to Webflow, Webflow stores the submission, optionally e-mails it to people, and exposes it to integrations, webhooks and the Data API. This page explains each stage, the limits of each stage, and the points at which a developer takes over.

The running example is Fernbrook Gardens, a fictitious botanical garden. It has two forms.

  • A group-visit enquiry form on the Visit page. A school teacher or a tour operator gives a name, an e-mail address, a group size, a preferred date and a message. It must reach the visitor services team within a minute.

  • A volunteer application form on the Volunteers page, which includes a file upload for a short curriculum vitae and a consent checkbox. It must be stored, forwarded to the volunteer coordinator and never exposed publicly.

What a Webflow form is

A Webflow form is an ordinary HTML form element that the Designer builds for you and that the Webflow runtime script handles on the published page. Understanding the three parts avoids most surprises.

  1. The markup. The Form block is a wrapper div that contains a form element, a success message div and an error message div. The form element holds the labels, inputs and a submit button.

  2. The runtime. When the visitor submits, webflow.js intercepts the event, validates required fields, sends the data to Webflow in the background (an asynchronous request, not a page navigation) and then shows the success or the error message, or follows the redirect you configured. Because the page does not reload, the visitor stays where they are.

  3. The service. Webflow’s form service receives the data, applies spam filtering, stores the submission against the site and form name, sends the notification e-mails and calls whatever integrations or webhooks are connected.

The following markup is what the Designer generates for a minimal enquiry form. Class names are examples; the structure is what matters. Attribute names such as data-name, data-wf-page-id and data-wf-element-id are written by Webflow and should be left alone.

<div class="enquiry_wrapper w-form">
  <form id="wf-form-Group-Visit-Enquiry" name="wf-form-Group-Visit-Enquiry"
        data-name="Group Visit Enquiry" method="get" class="enquiry_form"
        data-wf-page-id="PAGE_ID_PLACEHOLDER" data-wf-element-id="ELEMENT_ID_PLACEHOLDER">
    <label for="Name">Your name</label>
    <input class="w-input" maxlength="256" name="Name" data-name="Name"
           placeholder="" type="text" id="Name" required="">

    <label for="Email">E-mail address</label>
    <input class="w-input" maxlength="256" name="Email" data-name="Email"
           placeholder="" type="email" id="Email" required="">

    <label for="Group-size">Group size</label>
    <input class="w-input" name="Group-size" data-name="Group size"
           type="number" id="Group-size" required="">

    <label for="Message">Message</label>
    <textarea id="Message" name="Message" maxlength="5000" data-name="Message"
              placeholder="" class="w-input"></textarea>

    <input type="submit" data-wait="Please wait..." class="w-button" value="Send enquiry">
  </form>
  <div class="w-form-done" tabindex="-1" role="region" aria-label="Group Visit Enquiry success">
    <div>Thank you. Visitor services will reply within two working days.</div>
  </div>
  <div class="w-form-fail" tabindex="-1" role="region" aria-label="Group Visit Enquiry failure">
    <div>Something went wrong. Please e-mail visits@fernbrook.example instead.</div>
  </div>
</div>

Three details in that markup deserve attention.

  • The method attribute reads get in the generated HTML even though the runtime posts the data. The runtime intercepts the submit event, so the attribute only matters if JavaScript fails to load. Do not infer the transport from it.

  • The name attribute of each input is the key under which the value appears in the submission, in the notification e-mail and in the webhook payload. A field named Group-size is reported as Group size in the display name. Name fields deliberately before collecting data, because renaming a field later splits the history across two columns.

  • The success and error blocks are normal elements with the classes w-form-done and w-form-fail. They are hidden by default and shown by the runtime. You style them like any other element, and you can put links, icons or a second form inside them.

Building a form in the Designer

The Add panel contains a Form block and individual form elements. Adding the Form block gives a ready-made form with a name, an e-mail field, a message and a button; you then delete, reorder or restyle the pieces. The elements available at the check date are the following.

Element What it produces Typical Fernbrook use

Form block

The wrapper, the form element, the success message and the error message

Both forms

Label

A label element tied to an input with the for attribute

Every field

Text field

An input of type text with a name, a placeholder, a maximum length and a required switch

Visitor name

E-mail field

An input of type email; the browser validates the shape of the address

Contact address

Text area

A textarea with a maximum length

Enquiry message

Select

A select element with a list of options that you type in

Preferred visit month

Checkbox

An input of type checkbox with its own label

Newsletter and consent

Radio button

A group of input elements of type radio sharing one name

Group type: school, tour operator, other

File upload

A file chooser that uploads the file to Webflow and stores a link in the submission

Volunteer curriculum vitae

reCAPTCHA

A Google reCAPTCHA challenge, described below

Optional

Submit button

An input of type submit with a waiting label, or a button element

Both forms

The two inputs that matter most for quality are the label and the name. A placeholder is not a label: it disappears as the visitor types and assistive technology does not reliably announce it. The Accessibility page lists the form checks (visible labels, for and id pairs, error messages announced by assistive technology, sufficient contrast on focus rings) that the Audit panel cannot make for you.

Field settings that change behaviour

Each input has settings in the Element settings panel. These are the ones that change what reaches Webflow.

  • Name. The key of the field. It is used verbatim in notifications, exports and webhooks.

  • Required. Adds the required attribute. The browser blocks the submission and shows its own message; the runtime also checks required fields. Required is a convenience for visitors, not a security control, because a script can post directly to the endpoint without running either check.

  • Max length. Adds maxlength. The same caveat applies.

  • Type. Text, e-mail, password, tel, URL, number or other HTML input types are available. A type changes the mobile keyboard and the browser validation, nothing more.

  • Autofocus and autocomplete. Set autocomplete values such as name, email and tel so browsers and password managers fill the fields. This is an accessibility requirement for many users and costs nothing.

Form settings

Selecting the form (not the block) shows the form settings.

  • Name. The label under which submissions are grouped in the site settings, in exports and in the notification subject. Two forms with the same name merge into one list. Give every form a distinct, stable name.

  • Action and Method. Left empty, the form posts to Webflow. Filling the action turns the form into a custom form action, covered below.

  • Redirect URL. A page to open after a successful submission instead of showing the success message. Use it to send visitors to a thank-you page where analytics can count a conversion by page view.

  • Form notifications and Form submissions switches. Whether Webflow stores the submission and e-mails it, covered below.

Success and error states

The success and the error blocks are part of the Form block, and the Designer lets you preview them by switching a form state in the Element settings panel or by using the state selector on the block. Treat them as content, not decoration.

A good success message does four things: it confirms that the submission arrived, states what happens next, says when, and offers a way out if the visitor made a mistake. The Fernbrook enquiry message therefore reads "Thank you. Visitor services will reply within two working days" rather than "Success". A good error message says what the visitor can do now. Because the runtime shows the error block when the request fails (for example when the visitor is offline, when the spam filter rejects the submission or when a required reCAPTCHA is missing), it should always offer an alternative channel.

The Webflow runtime hides the form element and reveals the success block, or follows the redirect. Two accessibility details are worth setting by hand:

  • give each of the two blocks an accessible name and a landmark role such as region, as in the markup above, so screen reader users hear the result;

  • when the success block appears, move focus to it. The generated blocks carry tabindex="-1" so a small script can call focus() on them; without that, a keyboard user is left on a button that has vanished.

// Move focus to the success or the error message after a Webflow form submission.
// Webflow toggles the display of .w-form-done and .w-form-fail.
document.addEventListener("submit", (event) => {
  const form = event.target.closest(".w-form");
  if (!form) return;

  const observer = new MutationObserver(() => {
    const done = form.querySelector(".w-form-done");
    const fail = form.querySelector(".w-form-fail");
    for (const box of [done, fail]) {
      if (box && getComputedStyle(box).display !== "none") {
        box.focus();
        observer.disconnect();
        return;
      }
    }
  });
  observer.observe(form, { attributes: true, subtree: true, attributeFilter: ["style"] });
});

This script is site code, not a Webflow feature. It goes in the page or the site footer code as described on Custom Code in the Head, Footer and Embeds.

File upload

The File upload element lets the visitor attach a file. It is the part of Webflow forms that people most often misjudge, because it behaves differently from every other field.

  • The browser sends the file to Webflow’s form service together with the other fields. Webflow stores the file and puts a URL into the submission value. The file itself is not in the notification e-mail; the e-mail and the exported data contain a link.

  • There is a per-file size limit, which was 10 MB at the check date. It is enforced by the service and cannot be changed in the Designer. Files above the limit fail the submission, so state the limit next to the field.

  • File upload fields are available on the plans that include form submissions; check the pricing page for the current availability of the feature on your tier before promising it to a client.

  • There is no setting to restrict file types in the element. The accept attribute can be added as a custom attribute (see Custom Attributes and JavaScript), but it is a convenience for the file chooser, not validation, because a script can post any file.

  • The links in the export are not guaranteed to live forever. Webflow’s guidance is to copy uploaded files into your own storage, for example with an automation, if you need to keep them.

<label for="Cv">Curriculum vitae (PDF, up to 10 MB)</label>
<input type="file" id="Cv" name="Cv" data-name="Cv" accept=".pdf,application/pdf"
       aria-describedby="Cv-help">
<p id="Cv-help">We keep your file for the length of the volunteer season and delete it afterwards.</p>

Because the uploaded file is reachable by anyone who has its URL, the volunteer form should not collect anything the organisation would not want to see in a leaked link. Ask for a short CV, not identity documents, and move the file into private storage as soon as the notification arrives (see the receiver example below).

Submissions: where they go and how long they stay

Every submission to the default Webflow action is stored against the site. The submissions are listed in the Designer under Site settings, in the Forms tab, where each form name has its own table. The same tab lets you:

  • read each submission, with the date, the field values and any file links;

  • export all the submissions of a form as a CSV file with the Export all action in the submissions tab;

  • delete individual submissions, which is the practical way to honour an erasure request;

  • configure the integrations (Zapier, Make and other tools appear in the Apps & Integrations area) that react to new submissions.

Content editors with a seat can view submissions from the canvas as well. Which roles can see form data depends on the seat and role: a Reviewer cannot, a Content editor with form access can. See Plans, Workspaces, Seats and Roles for the role matrix.

Limits

The limits are tied to the Site plan of the site, not to the Workspace.

Plan tier Form submissions

Starter (unhosted site on a free plan)

50 submissions in total for the life of the site. The counter is never reset

Basic and Premium

A monthly allowance that resets every month

Team

A monthly allowance that resets every month

Enterprise

Negotiated with the contract

Ecommerce tiers

A monthly allowance that resets every month

The practical meaning is that a Starter site is for testing. Fernbrook Gardens builds both forms on a Starter site in the Developer workspace, tests them with a handful of submissions, and moves to a paid Site plan before launch. Once the allowance is exhausted, new submissions are rejected and the visitor sees the error block, so monitor usage on busy sites. When the allowance is a risk, choose a custom action (below) so that the volume is handled by a service you control.

The retention period of the data is yours to define in your privacy policy. Export and archive submissions if that period is shorter or longer than the life of the site.

Notifications

By default a new form sends each submission by e-mail to the address of the account that owns the site. You change this in the form settings.

  • Recipients. One or more e-mail addresses. On a free Starter Site plan the notification e-mails are limited to five recipients and every recipient must be a member of the Workspace. Paid Site plans can notify addresses that are not Workspace members.

  • Subject. A fixed text, optionally with field values inserted so the coordinator can triage from the inbox. For Fernbrook, "Group visit enquiry from" followed by the inserted name field shows who wrote.

  • Sender name and reply-to. The sender name appears in the inbox. The reply-to field can be the visitor’s e-mail address, so that pressing Reply answers the visitor. Set a monitored mailbox as the sender; do not use the visitor’s address as the From address, which fails mail authentication checks.

  • Message. The e-mail body contains all the submitted fields.

Notification e-mails are a convenience, not a queue. They can be filtered into spam folders, delayed or lost. Anything important should also reach a system with a record and an owner: the submissions list, a webhook receiver or an integration that creates a ticket.

Setting combination Result

Form submissions on, notifications on (default)

Submission stored and e-mailed

Form submissions on, notifications off

Submission stored only; useful when a webhook or integration does the alerting

Custom action set

Webflow neither stores nor e-mails; the custom endpoint receives everything

Spam protection

A public form attracts bots within days. Webflow offers four layers, each with a different cost and a different failure mode. Use them together.

Layer How it works Trade-off

Cloudflare Turnstile bot blocking and spam filtering

A site-level setting. Bot blocking uses Cloudflare’s invisible challenge; spam filtering inspects the submitted data and discards submissions that look like spam

Invisible to visitors. Spam filtering can discard a legitimate message that looks like spam, so watch for false positives

Honeypot

A hidden field that humans never fill; a submission with that field filled is treated as spam

Needs custom code. Weak against bots that do not fill every field

reCAPTCHA element

A visible challenge from Google that the visitor completes before submitting

Friction for visitors; enabling it applies to every form on the site; third-party cookies and a privacy review

Server-side validation

A custom endpoint or a webhook receiver rejects bad data after the submission

You operate it

Cloudflare Turnstile

Turnstile is Webflow’s native, invisible spam protection. It is on by default for new sites and is managed from Site settings, in the Apps & Integrations area, under Cloudflare Turnstile spam protection, where bot blocking and spam filtering are two separate switches.

  • Bot blocking asks Turnstile to decide whether the request comes from a real browser. Visitors do not see a challenge.

  • Spam filtering checks all the data in a submission to decide whether it is spam.

Because Turnstile is a site-level setting, no change to the form is required, and it works with the default Webflow action only. If you use a custom action, Webflow is not in the path and you must add your own protection at the endpoint.

Honeypot

A honeypot is a field that real visitors cannot see but that simple bots fill in. Webflow does not provide an element for it, so you build it: add a text input to the form, hide it with a class that moves it off screen (do not use display: none, which many bots detect), mark it so assistive technology and browsers ignore it, and block the submission when it is filled.

<div class="hp_field" aria-hidden="true">
  <label for="Website">Leave this field empty</label>
  <input type="text" id="Website" name="Website" data-name="Website"
         tabindex="-1" autocomplete="off">
</div>
.hp_field {
  position: absolute;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
// Block submission when the honeypot is filled. Capture phase runs before webflow.js.
document.addEventListener(
  "submit",
  (event) => {
    const form = event.target;
    const trap = form.querySelector('input[name="Website"]');
    if (trap && trap.value.trim() !== "") {
      event.preventDefault();
      event.stopImmediatePropagation();
      // Pretend success so the bot learns nothing.
      const done = form.parentElement.querySelector(".w-form-done");
      if (done) {
        form.style.display = "none";
        done.style.display = "block";
      }
    }
  },
  true
);

Two cautions apply. A browser that autofills every field can fill the honeypot, so give it an unusual name (not address or email) and autocomplete="off". And because the check runs in the browser, a bot that posts directly to the endpoint bypasses it, which is why the honeypot complements Turnstile rather than replacing it.

reCAPTCHA

The reCAPTCHA element adds Google’s challenge to a form. Setting it up has two parts: create a reCAPTCHA key pair in Google’s admin console, and enter the site key and the secret key in Site settings, in the Forms tab. Then add the reCAPTCHA element to each form. The rules to know are the following.

  • Enabling it is site-wide. Once the keys are entered and validation is switched on, every form on the site is validated against reCAPTCHA. A form without the element fails to submit. Add the element to all forms first, then enable the validation.

  • Native support is for the v2 checkbox type. Create the keys as reCAPTCHA v2 checkbox keys. Other key types do not work with the element.

  • It does not work with a custom form action. The check is done by Webflow’s form service, which a custom action bypasses.

  • It does not work on exported sites. Code export drops form processing and reCAPTCHA, see Code Export and Portability.

  • It adds a third party. Google sets cookies and processes visitor data; reflect that in the consent banner and the privacy notice (see below).

Fernbrook Gardens turns on Turnstile for both forms, adds a honeypot to the group-visit enquiry (a public form with an open text area) and leaves reCAPTCHA off, because the extra friction would lose genuine teachers. The volunteer form sits behind the password-protected Volunteers area, which removes most bot traffic.

The submission flow

The diagram shows the default path of one submission, and where each protection acts.

sequenceDiagram autonumber actor V as Visitor participant B as Browser (webflow.js) participant W as Webflow form service participant T as Turnstile participant S as Submissions store participant M as E-mail participant H as Webhook receiver V->>B: Fill the form and press Send B->>B: Browser validation (required, type, maxlength) B->>B: Honeypot script (if added) B->>W: Asynchronous POST of the fields (and files) W->>T: Bot blocking and spam filtering alt Rejected as spam or limit reached W-->>B: Failure response B-->>V: Error message block else Accepted W->>S: Store the submission W->>M: Send the notification W-->>B: Success response B-->>V: Success message block or redirect W-)H: form_submission webhook (if configured) end

Reading the diagram from the top: everything before step 5 runs in the visitor’s browser and can be bypassed by a script; everything from step 5 runs on Webflow’s side and is the part you can rely on. The webhook is delivered after the response, so a slow or failing receiver never makes the visitor wait.

Custom form actions

The Action setting turns the form into an ordinary HTML form that posts to a URL you provide. Developers use it to send submissions to their own endpoint, to a form-handling service or to a Webflow Cloud route. The consequences are large, so decide deliberately.

What changes Detail

Webflow no longer stores the submission

It does not appear in Site settings and does not count against the allowance

Notifications and Webflow integrations stop

The custom action cannot be combined with Webflow or e-mail notifications

Turnstile does not apply

Webflow is not in the request path

The reCAPTCHA element does not work

Validation is done by Webflow’s service

The form_submission webhook does not fire

Webflow never sees the submission

The success and error blocks and the redirect URL may stop working

Webflow’s handling applies to submissions sent to Webflow; with a custom action you manage the outcome

If you still want the Webflow experience (the success block, no page navigation) with your own endpoint, intercept the submit event and send the data with fetch, leaving the form’s action empty so that nothing is posted to Webflow twice. The example below posts to a Fernbrook endpoint and reveals the Webflow blocks manually.

// Send the group-visit enquiry to a Fernbrook endpoint and keep the Webflow success and error blocks.
const ENDPOINT = "https://enquiries.fernbrook.example/api/group-visit"; // placeholder

document.addEventListener(
  "submit",
  async (event) => {
    const form = event.target;
    if (form.dataset.name !== "Group Visit Enquiry") return;

    event.preventDefault();
    event.stopImmediatePropagation(); // keep webflow.js from also posting to Webflow

    const wrapper = form.closest(".w-form");
    const done = wrapper.querySelector(".w-form-done");
    const fail = wrapper.querySelector(".w-form-fail");
    const button = form.querySelector('input[type="submit"]');
    const label = button.value;
    button.value = button.dataset.wait || "Please wait...";
    button.disabled = true;

    try {
      const response = await fetch(ENDPOINT, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(Object.fromEntries(new FormData(form))),
      });
      if (!response.ok) throw new Error("HTTP " + response.status);
      form.style.display = "none";
      done.style.display = "block";
      done.focus();
    } catch (error) {
      fail.style.display = "block";
      fail.focus();
    } finally {
      button.value = label;
      button.disabled = false;
    }
  },
  true
);

The endpoint must allow cross-origin requests from the Fernbrook domain, rate-limit by address, validate every field on the server and apply its own spam protection. A Fernbrook-owned endpoint can be built as a Webflow Cloud route, see Webflow Cloud.

A different pattern keeps Webflow as the primary store and uses the data afterwards, which is usually the better choice: leave the action empty, and consume the submissions through the webhook or the Data API described next. That keeps Turnstile, the allowance accounting and the Site settings list.

Forms and integrations

Webflow connects the default action to other systems in three ways.

  1. No-code integrations. Automation tools such as Zapier and Make, and CRM tools with Webflow connectors, can trigger on a new form submission and create a row, a ticket or a contact. They listen to the same events as a webhook, but are configured in the tool.

  2. Webhooks. A site webhook with the form_submission trigger calls a URL of yours for each submission. This is the developer route and the subject of the next section.

  3. The Data API. An application reads or deletes stored submissions on demand. Section after next.

The Help Center previously described Webflow Logic flows that started from a form. Logic was sunset on 2025-06-27, its flows were disabled and forms that used it reverted to standard forms; see Product Changes and Deprecations. Replace such flows with an automation tool or a webhook receiver. Likewise the Memberships and User Accounts product, which let a form create a user, was sunset on 2026-01-29.

See Integrations and Marketplace Apps for the catalogue of connectors, and Webhooks for the full webhook model.

The form_submission webhook

A webhook is registered for a site and a trigger type. The form_submission trigger fires after each accepted submission. It requires the forms:read scope on the token that registers it. The optional filter limits it to one form by name, which is handy on a site with several forms.

# Register a form_submission webhook for the Fernbrook Gardens site (placeholders only).
curl --request POST \
  --url https://api.webflow.com/v2/sites/SITE_ID_PLACEHOLDER/webhooks \
  --header "Authorization: Bearer WEBFLOW_SITE_TOKEN_PLACEHOLDER" \
  --header "Content-Type: application/json" \
  --data '{
    "triggerType": "form_submission",
    "url": "https://hooks.fernbrook.example/webflow/forms",
    "filter": { "name": "Volunteer Application" }
  }'

The request that Webflow then sends is a JSON body with the trigger type and a payload.

{
  "triggerType": "form_submission",
  "payload": {
    "name": "Volunteer Application",
    "siteId": "SITE_ID_PLACEHOLDER",
    "data": {
      "Name": "Ada Example",
      "Email": "ada@example.org",
      "Cv": "https://uploads.example/placeholder-cv.pdf",
      "Consent": "true"
    },
    "schema": [
      { "fieldName": "Name", "fieldType": "FormTextInput", "fieldElementId": "ELEMENT_ID_1" }
    ],
    "submittedAt": "2026-10-28T09:30:00.000Z",
    "id": "EVENT_ID_PLACEHOLDER",
    "formId": "FORM_ID_PLACEHOLDER",
    "formElementId": "FORM_ELEMENT_ID_PLACEHOLDER"
  }
}

The values in data are keyed by field name; the schema array describes each field. Treat all of it as untrusted visitor input.

Verifying the signature

Requests carry two headers: x-webflow-timestamp, the time the webhook was sent, and x-webflow-signature, a SHA-256 HMAC. The signature is computed over the timestamp, a colon and the raw request body. The signing key is the secret of the webhook for webhooks created through the API with a site token after 2025-04-14, or the client secret of the app for OAuth app webhooks. Webhooks created in the Webflow dashboard do not carry the signature headers and cannot be verified this way, so create production webhooks through the API. Reject requests whose timestamp is more than five minutes old, which blocks replay.

The following Node and TypeScript receiver verifies the signature, ignores duplicates by event id and moves the uploaded file to private storage. It uses only the standard library; the Webflow SDK also provides a signature verification helper that you may prefer.

import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.WEBFLOW_WEBHOOK_SECRET ?? ""; // never commit a real secret
const seen = new Set<string>(); // use a database in production

// Keep the raw body: the signature covers the exact bytes.
app.post("/webflow/forms", express.raw({ type: "application/json" }), async (req, res) => {
  const timestamp = String(req.header("x-webflow-timestamp") ?? "");
  const signature = String(req.header("x-webflow-signature") ?? "");
  const body = req.body as Buffer;

  // The header is documented as Unix epoch time; accept seconds or milliseconds.
  const sent = Number(timestamp);
  const sentMs = sent > 1e12 ? sent : sent * 1000;
  const ageMs = Date.now() - sentMs;
  if (!timestamp || !Number.isFinite(ageMs) || Math.abs(ageMs) > 5 * 60 * 1000) {
    return res.status(400).send("stale");
  }

  const expected = createHmac("sha256", SECRET)
    .update(`${timestamp}:${body.toString("utf8")}`)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return res.status(401).send("bad signature");
  }

  const event = JSON.parse(body.toString("utf8"));
  const { id, name, data } = event.payload;
  if (seen.has(id)) return res.status(200).send("duplicate");
  seen.add(id);

  try {
    if (name === "Volunteer Application" && typeof data.Cv === "string") {
      await copyToPrivateStorage(data.Cv, `volunteers/${id}.pdf`);
    }
    await notifyCoordinator(name, data);
  } catch (err) {
    seen.delete(id); // let Webflow's retry process the submission again
    return res.status(500).send("retry");
  }

  // Answer quickly with 200; do slow work asynchronously.
  return res.status(200).send("ok");
});

async function copyToPrivateStorage(url: string, key: string): Promise<void> {
  /* download the file and store it where only the coordinator can read it */
}
async function notifyCoordinator(form: string, data: Record<string, unknown>): Promise<void> {
  /* create a ticket or send a message to the volunteer coordinator */
}

app.listen(3000);

Delivery rules matter for receiver design. Webflow treats a non-200 response, a redirect, an SSL certificate problem or a timeout as a failure and retries the delivery up to three more times, ten minutes apart. A webhook that keeps failing is deactivated and Webflow e-mails the owner. A site can register up to 75 webhooks per trigger type. A receiver must therefore answer fast, be idempotent (the same event id may arrive more than once) and monitor its own availability. The page Webhooks covers the model in full.

Reading and deleting submissions with the Data API

The Data API lets an application list the stored submissions of a site or of one form, fetch one, and delete one. This is the route for dashboards, back-fills after a receiver outage, and erasure requests.

  • List by site: GET /v2/sites/{site_id}/form_submissions, scope forms:read. The optional elementId query parameter filters to one form; take it from the list forms endpoint, where it is called formElementId. A form that lives in a component produces one form per component instance; passing the core form’s elementId returns the submissions of all its instances. limit is at most 100 and offset pages through the results.

  • The response holds formSubmissions and pagination. Each submission has an id, the form name as displayName, the siteId, the workspaceId, dateSubmitted, the field values in formResponse and a localeId that is null for the primary locale or non-localized sites.

  • Deleting a submission requires the forms:write scope.

import { WebflowClient } from "webflow-api";

const client = new WebflowClient({ accessToken: process.env.WEBFLOW_TOKEN ?? "" });
const SITE_ID = "SITE_ID_PLACEHOLDER";

// Page through every submission of the site and print a summary.
let offset = 0;
for (;;) {
  const page = await client.sites.forms.listSubmissionsBySite(SITE_ID, { limit: 100, offset });
  const items = page.formSubmissions ?? [];
  for (const submission of items) {
    console.log(submission.dateSubmitted, submission.displayName, submission.formResponse);
  }
  offset += items.length;
  if (items.length === 0 || offset >= (page.pagination?.total ?? 0)) break;
}
import os
import requests

SITE_ID = "SITE_ID_PLACEHOLDER"
URL = f"https://api.webflow.com/v2/sites/{SITE_ID}/form_submissions"
HEADERS = {"Authorization": f"Bearer {os.environ['WEBFLOW_TOKEN']}"}

offset = 0
while True:
    r = requests.get(URL, headers=HEADERS, params={"limit": 100, "offset": offset}, timeout=30)
    r.raise_for_status()                      # 429 means slow down: read X-RateLimit-Remaining
    body = r.json()
    items = body.get("formSubmissions", [])
    for s in items:
        print(s["dateSubmitted"], s["displayName"], s["formResponse"])
    offset += len(items)
    if not items or offset >= body["pagination"]["total"]:
        break

The request fails with a 401 for an invalid token and a 429 when the rate limit is hit. Rate limits and error handling are covered on Data API: SDKs, Limits and Errors, and the forms endpoints together with assets and custom code on Data API: Assets, Forms and Custom Code. Tokens and scopes are explained on Data API: Authentication and Scopes.

The Webflow CLI also has a forms command group for listing forms and submissions from a terminal; see Webflow CLI.

Forms inside components, CMS templates and localized sites

  • Components. A form can live inside a component. Each instance on the page is a separate form with its own submissions list entry unless the instances share the form name, so give the form in the component definition a clear name and remember the elementId behaviour described above. See Components, Props, Slots and Variants.

  • Collection template pages. A form on a template page is repeated for every item. If the form should know which plant it is about, add a hidden input and bind its value to a CMS field in the dynamic embed or in the field setting, so the submission arrives with the plant name. See Collection Template Pages.

  • Localized sites. Forms exist in each locale, and the submission records its locale; success messages and labels must be translated in each locale. See Localization.

<!-- A hidden field on the Plants template page; the value is bound to the Name field of the item. -->
<input type="hidden" name="Plant" data-name="Plant" value="Japanese maple">

A form collects personal data, so it creates obligations that no spam setting can satisfy. The legal requirements depend on the jurisdiction and are not given advice here; the engineering consequences are the following.

  • Say what you collect and why, next to the form. A short sentence and a link to the privacy notice. Link it from a required checkbox when your legal basis is consent. Store the consent as its own field so that it is part of every record.

  • Collect the minimum. Each optional field increases risk. The Fernbrook enquiry form needs no date of birth.

  • Know where the data lives. Submissions are stored by Webflow; notification e-mails live in mailboxes; files live at public URLs; a webhook receiver and any automation tool hold further copies. List every copy in the record of processing, and delete from all of them on an erasure request.

  • Delete or export on request. Use the submissions list in Site settings or the Data API to export and to delete.

  • Third parties need consent too. reCAPTCHA loads Google scripts and sets cookies. Turnstile is invisible to visitors but still a third party. Reflect both in the consent banner and in the privacy notice, and see the consent API in Third-Party Libraries and Scripts.

  • Protect the files. An uploaded file is reachable through its link, so treat the link as a secret, copy the file into private storage and do not paste the link into public tools.

  • Limit who can read submissions. Give form access only to the seats that need it, and review the list when people leave the team.

Testing forms

Test forms on the staging domain before launch and again after every change to spam settings.

  1. Submit a valid entry from a desktop and a phone browser. Check that the submission appears in Site settings, the e-mail arrives (and is not in spam), the redirect or success block shows, and the webhook receiver logs the event.

  2. Submit with each required field empty and confirm the browser messages are understandable.

  3. Submit a file above the size limit and a file of the wrong type, and confirm the message the visitor sees.

  4. Fill the honeypot from the developer tools and confirm nothing reaches the submissions list.

  5. Turn off the network, submit, and confirm the error block offers another way to get in touch.

  6. Check the keyboard path: tab through the labels and fields, press Enter to submit, and confirm focus lands on the result message.

  7. Switch each locale and confirm the labels and messages are translated.

The preview in the Designer does not post to the production service for all features; the published staging site is the reliable place to test. A Starter site has only 50 submissions in total, so run tests sparingly or create the test submissions on a Developer workspace site and delete them afterwards.

Troubleshooting

Symptom Likely cause and fix

Every form on the site fails after enabling reCAPTCHA

The validation is site-wide; add the reCAPTCHA element to every form or switch validation off

Submissions arrive but no e-mail does

Notifications are off for the form, the recipient is not a Workspace member on a free Site plan, or the mail is in a spam folder

Nothing is stored after setting an action URL

A custom action replaces Webflow’s handling; remove the action to use Webflow storage

The success message never appears

A script stopped the event or a redirect is set; check the console and the form’s Redirect URL

The file upload fails

The file exceeds the size limit, or the connection dropped; show the limit next to the field

The webhook never fires

The form uses a custom action, the webhook was created with a filter naming a different form, or it was deactivated after repeated failures

The signature check always fails

The body was parsed before hashing, the secret is wrong, or the webhook was created in the dashboard and carries no signature headers

Submissions stop after weeks on a Starter site

The 50-submission total was reached; move to a paid Site plan or handle volume with your own endpoint

Legitimate messages are missing

Spam filtering discarded them; test with realistic text and review the setting

Corrections to older material

Older books and tutorials, including Webflow by Example (Packt, 2021), do not cover forms, so there is no direct contradiction. Older community answers, however, still describe three things that changed: the legacy Editor, which could show a form submissions list to editors and has been fully deprecated since 2026-10-05; Webflow Logic flows triggered by forms, sunset on 2025-06-27; and reCAPTCHA as the only built-in spam control, which Turnstile now complements. See Where Older Books and Tutorials Are Outdated.

Summary

  • A Webflow form is an HTML form that webflow.js posts to Webflow in the background; Webflow stores, filters and e-mails the submission and can call a webhook.

  • Name forms and fields on purpose; the names are the keys in every export, e-mail and webhook.

  • Starter allows 50 submissions in total with no reset; paid Site plans have a monthly allowance.

  • Layer the spam protection: Turnstile, a honeypot, reCAPTCHA only if needed (site-wide, not with a custom action), and validation on the server.

  • A custom action removes storage, notifications, Turnstile, reCAPTCHA and the webhook; prefer webhooks and the Data API.

  • Treat uploaded-file links and submissions as personal data and plan retention, deletion and consent.