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.
-
The markup. The Form block is a wrapper
divthat contains aformelement, a success messagedivand an error messagediv. The form element holds the labels, inputs and a submit button. -
The runtime. When the visitor submits,
webflow.jsintercepts 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. -
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
methodattribute readsgetin 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-sizeis reported asGroup sizein 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-doneandw-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 |
Both forms |
Label |
A |
Every field |
Text field |
An |
Visitor name |
E-mail field |
An |
Contact address |
Text area |
A |
Enquiry message |
Select |
A |
Preferred visit month |
Checkbox |
An |
Newsletter and consent |
Radio button |
A group of |
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 |
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
requiredattribute. 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
autocompletevalues such asname,emailandtelso 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 callfocus()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
acceptattribute 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.
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 |
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.
-
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.
-
Webhooks. A site webhook with the
form_submissiontrigger calls a URL of yours for each submission. This is the developer route and the subject of the next section. -
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, scopeforms:read. The optionalelementIdquery parameter filters to one form; take it from the list forms endpoint, where it is calledformElementId. A form that lives in a component produces one form per component instance; passing the core form’selementIdreturns the submissions of all its instances.limitis at most 100 andoffsetpages through the results. -
The response holds
formSubmissionsandpagination. Each submission has anid, the form name asdisplayName, thesiteId, theworkspaceId,dateSubmitted, the field values informResponseand alocaleIdthat is null for the primary locale or non-localized sites. -
Deleting a submission requires the
forms:writescope.
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
elementIdbehaviour 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">
Privacy and consent
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.
-
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.
-
Submit with each required field empty and confirm the browser messages are understandable.
-
Submit a file above the size limit and a file of the wrong type, and confirm the message the visitor sees.
-
Fill the honeypot from the developer tools and confirm nothing reaches the submissions list.
-
Turn off the network, submit, and confirm the error block offers another way to get in touch.
-
Check the keyboard path: tab through the labels and fields, press Enter to submit, and confirm focus lands on the result message.
-
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
formthatwebflow.jsposts 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.
Next steps: SEO and AEO, Webhooks and Data API: Assets, Forms and Custom Code.