Migrate · Netlify Forms
Migrate from Netlify Forms to Formward
Netlify Forms is not an endpoint you post to. Netlify finds forms in your built HTML when it deploys the site, and the browser posts back to your own pages. So the change is replacing Netlify's attributes with an action that points at Formward.
After the move the form no longer depends on Netlify's build: it works the same if the site moves to another host, and a form rendered by JavaScript does not need a hidden static copy in the HTML.
Last checked 7 October 2026. What this guide says about Netlify Forms comes from Netlify Forms's own documentation, listed under Sources. Check their current docs if something looks different in your account. The Formward side is the reference in the migration docs.
The change
Remove data-netlify, netlify-honeypot and any hidden form-name input, and add an action that points at Formward.
Before: Netlify Forms, HTML
<form name="contact" method="POST" action="/thanks" data-netlify="true" netlify-honeypot="bot-field">
<p hidden><label>Leave empty: <input name="bot-field"></label></p>
<input type="email" name="email" required>
<textarea name="message" required></textarea>
<input type="hidden" name="subject" value="New enquiry">
<button type="submit">Send</button>
</form>After: Formward, HTML
<form action="https://forms.formward.eu/f/<formId>" method="POST">
<input type="email" name="email" required>
<textarea name="message" required></textarea>
<input type="hidden" name="_subject" value="New enquiry">
<input type="hidden" name="_redirect" value="/thanks">
<input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off">
<button type="submit">Send</button>
</form>Before: Netlify Forms, fetch()
// The form includes <input type="hidden" name="form-name" value="contact">
await fetch("/", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams(new FormData(form)).toString(),
});After: Formward, fetch()
const res = await fetch("https://forms.formward.eu/f/<formId>", {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
});
const data = await res.json();
if (!data.ok) throw new Error(data.error);Field and feature mapping
| Netlify Forms | Formward | Notes |
|---|---|---|
| data-netlify="true" | action="https://forms.formward.eu/f/<formId>" | The form ID in the URL identifies the form. |
| form-name hidden input | remove | Not needed. If you keep it, it is stored as an ordinary field. |
| netlify-honeypot="bot-field" | _gotcha | Rename the hidden input; the form attribute goes. |
| action="/thanks" | _redirect | Or set the redirect URL on the form. Relative paths resolve against your site. |
| subject | _subject | Subject of the notification email. |
| email (reply-to) | email (same) | Netlify uses an email field as Reply-To, and so does Formward; _replyto overrides it. |
| data-netlify-recaptcha | cf-turnstile-response | Formward offers Cloudflare Turnstile, optional per form. |
| file input | file input, multipart | Uploads are on Professional and up (5 files of up to 10 MiB each on Professional). |
What changes, what stays
Changes
- data-netlify, netlify-honeypot and form-name are removed, and an action is added.
- The honeypot input is renamed to _gotcha and the subject field to _subject.
- A success page set through the form's action moves to _redirect or the form's redirect setting.
- fetch() posts to the Formward endpoint instead of to your own site.
Stays the same
- Your field names and the visible markup.
- The site build: Formward needs nothing at deploy time, so no redeploy is needed for it to detect a form.
Differences to test before you switch
Specific to Netlify Forms
- Netlify sets the notification's Reply-To from a field named email. Formward does the same, so the field keeps its name; _replyto only overrides it.
- Netlify rejects honeypot and reCAPTCHA failures without listing them, and files Akismet matches under a separate spam list. Formward keeps honeypot hits in the form's spam folder, so you can check for false positives.
- A fetch() to / was a same-origin request. The Formward endpoint is on another origin, so keep the Accept: application/json header and add your site to the form's allowed origins.
- Netlify can run a serverless function when a submission is verified. With Formward, point a webhook at the function instead.
How Formward behaves
- JSON responses. Send Accept: application/json and a success is a 200 with { ok: true, id }. An error is { ok: false, error } with a status code. Without that header, a successful post redirects or shows Formward's thanks page. Code that only checks res.ok keeps working.
- Status codes. 402 when the plan's monthly submission limit is reached (the body has upgrade: true), 422 when a field fails a validation rule set on the form or required consent is missing, 429 when one address posts too often (with a Retry-After header), and 403 when the origin is not allowed, the Turnstile check fails or files are sent on a plan without uploads.
- File uploads. Uploads need enctype="multipart/form-data" and a plan that includes them: Professional and up (5 files of up to 10 MiB each on Professional). On Free and Personal a post that carries files is refused with 403, so test an upload before you switch a form that takes attachments.
- Redirects. A hidden _redirect is followed only if it is a relative path or a URL on one of the form's allowed origins; otherwise it is ignored and the redirect set on the form applies, then Formward's thanks page. JSON (AJAX) requests are never redirected.
- Spam. If the _gotcha honeypot is filled in, the sender gets a normal success response, but the submission is stored as spam, sends no notification and does not count toward your quota. Rate limits apply on every plan; Cloudflare Turnstile and disposable-email blocking are optional per form.
- Allowed origins. With the list empty, any site can post to the endpoint. Once you add an origin, requests whose Origin header does not match are refused with 403, including server-side requests that send no Origin header.
- Reply-To. The Reply-To of the notification email is taken from _replyto, then _reply_to, then a field named email. So a plain email field sets Reply-To with no rename, and _replyto only overrides it, for example when the email field has another name or the form has several. An invalid address is dropped, and the email then has no Reply-To.
Every status code and body is listed in responses and redirects.
History, integrations and settings
- Netlify's docs say verified submissions can be downloaded as CSV from the form's page in the Netlify UI; spam submissions are not included. Formward's CSV importer (Dashboard, Import & migrate) takes any CSV with a header row, up to 5,000 rows and 10 MB per file. Imported rows send no email and do not count toward your quota. Each row keeps its original date when the file has a column named Submitted At, created_at or createdAt; otherwise it is dated at import time. Attached files are not imported, only cell values.
- Forms on the Free plan keep submissions for 30 days, and the nightly retention sweep deletes imported rows older than a form's retention window. To keep a longer history, import into a form on a paid plan with a long enough window.
- Email notifications, outgoing webhooks, Slack notifications and submission-triggered functions belong to your Netlify account and do not move with the endpoint. Recreate the ones you need: Formward webhooks (including Slack, Discord, Teams and Mattermost), Google Sheets and Baserow sync start on Professional, the autoresponder on Professional, and extra notification recipients on Personal.
Pricing and data location
- On Netlify's credit-based plans, Forms are free and unlimited and you pay for hosting; older legacy plans meter form submissions per site.
- Netlify, Inc. is based in San Francisco. We did not find an EU-only storage option for form submissions on its site.
- Formward is priced in euros: Free with 50 submissions a month, then Personal at EUR 15, Professional at EUR 30 and Business at EUR 90 a month (EUR 10, 20 and 60 billed yearly).
- Formward stores and processes submissions at Hostup AB in Sweden and sends notification email through Mailjet in the EU. The one optional non-EU sub-processor is Cloudflare Turnstile, on forms where you switch it on.
The full side-by-side, with sources, is in Formward vs Netlify Forms. Formward's prices are on the plans page, and where each part of Formward runs is on EU data residency.
Step-by-step checklist
- Create the form. In the Formward dashboard, create a form, set the notification email and copy the endpoint.
- Edit the form tag. Remove data-netlify and netlify-honeypot, delete any hidden form-name input, and add the Formward endpoint as the action.
- Rename the fields. Rename the honeypot input to _gotcha and subject to _subject. Leave the visitor's email field named email; it still sets Reply-To. Move a success path from the action to _redirect.
- Update JavaScript submits. If you post with fetch() to /, post to the endpoint instead with an Accept: application/json header.
- Restrict origins. Add your site's origin, for example https://www.example.com, to the form's allowed origins. Leave the list empty if you also post from a server.
- Test before you switch. Point a copy of the form at Formward and send a few entries: a normal one, one with the honeypot filled, and one with a file if the form takes uploads. Check the dashboard and the notification email.
- Recreate notifications. Set up webhooks, including one to any function that used to run on submission, and extra recipients on the Formward form.
- Bring the history. If you want past submissions, export them as CSV from your old provider and import them under Dashboard, Import & migrate.
- Retire the Netlify form. Switch the live form, watch the first real submissions arrive, then disable the form at Netlify so nothing is split between two inboxes.
Sources
Last checked 7 October 2026
Move one form first
The free plan takes 50 submissions a month with no card. Point a copy of one form at Formward, send a few test entries, and switch the live form once the path works end to end.