Skip to content
← All migration guides

Migrate · Formspree

Migrate from Formspree to Formward

Formspree is the closest match to Formward's model: a plain HTML form or a fetch() call posts to an endpoint, and both read _gotcha as the honeypot. Formspree's current docs set the subject with a field named subject, use a field named email as the Reply-To address, and configure the thank-you redirect in its dashboard.

Formward's dashboard has a Formspree endpoint rewriter that turns a formspree.io URL into your Formward endpoint and a drop-in snippet, and its form importer reads your Formspree HTML or a formspree.json file.

Last checked 7 October 2026. What this guide says about Formspree comes from Formspree'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

Swap the action URL and rename subject to _subject. A field named email keeps working as Reply-To, so there is nothing to rename for it.

Before: Formspree, HTML

<form action="https://formspree.io/f/YOUR_FORM_ID" method="POST">
  <input type="email" name="email" required>
  <textarea name="message" required></textarea>
  <input type="hidden" name="subject" value="New enquiry">
  <input type="text" name="_gotcha" style="display:none">
  <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: Formspree, fetch()

const res = await fetch("https://formspree.io/f/YOUR_FORM_ID", {
  method: "POST",
  headers: { Accept: "application/json" },
  body: new FormData(form),
});
if (!res.ok) throw new Error("Submission failed");

After: Formward, fetch()

const res = await fetch("https://forms.formward.eu/f/<formId>", {
  method: "POST",
  headers: { Accept: "application/json" },
  body: new FormData(form),
});
if (!res.ok) throw new Error("Submission failed");

Field and feature mapping

FormspreeFormwardNotes
https://formspree.io/f/{id}https://forms.formward.eu/f/<formId>The only required change for most forms.
subject_subjectSubject of the notification email.
email (Reply-To)email (same)A field named email sets Reply-To on Formward too; _replyto overrides it.
_gotcha_gotchaSame name.
Redirect in form settings_redirect or the form's redirectA relative path, or a URL on one of the form's allowed origins.
_languagenoneFormward's own thanks page follows the visitor's browser language: English, Swedish or German. Redirect to a page on your site for other languages.
g-recaptcha-response, h-captcha-response, cf-turnstile-responsecf-turnstile-responseFormward supports Cloudflare Turnstile, optional per form.
file input, multipartfile input, multipartSame markup. Uploads are on Professional and up (5 files of up to 10 MiB each on Professional).
@formspree/react, @formspree/ajaxfetch()There is no Formward client library to install; a fetch() call does the same job.

What changes, what stays

Changes

  • The action or fetch() URL.
  • subject becomes _subject. The email field stays as it is and still sets Reply-To.
  • The thank-you redirect moves from Formspree's settings to a _redirect field or the form's redirect setting.
  • Formspree's client libraries are replaced by a fetch() call.

Stays the same

  • The _gotcha honeypot.
  • Your other field names; every named field is stored as sent.
  • fetch() with Accept: application/json, and checks on res.ok.
  • multipart/form-data for forms with files.

Differences to test before you switch

Specific to Formspree

  • Formspree uses a field named email as the Reply-To address, and so does Formward, so replies keep going to the visitor with no rename. Add _replyto only to override it, for example when the email field has another name or the form has several.
  • Formspree's docs say it silently ignores submissions that fill _gotcha. Formward keeps them in the form's spam folder, without a notification and without counting them, so you can check for false positives.
  • Formspree includes file uploads from its Personal plan. Formward includes them from Professional, so a Personal-plan form that takes files needs Professional on Formward.
  • If you used @formspree/react, its hook gave you per-field errors. Formward's 422 response names the failing field in a field property, which you can show next to the input.

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

  • Formspree's docs describe CSV and JSON export on its Personal, Professional and Business plans, with field names as the CSV headers. Its Free plan has no export and keeps 30 days of history, so export before an account drops to Free. 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.
  • The form importer on the same page reads your Formspree HTML form or a formspree.json file and creates an inactive draft with the fields mapped. Notification addresses, webhooks, autoresponders and spam settings are listed for you to review and are never switched on by the import.
  • Plugins such as Slack, Google Sheets, Zapier and webhooks, the auto response and Form Rules belong to your Formspree 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

  • Formspree bills in US dollars: Free with 50 submissions a month, Personal at $15 a month (200), Professional at $30 (2,000) and Business at $90 (20,000).
  • Formspree says its services are hosted on Amazon Web Services in the United States and relies on Standard Contractual Clauses for EU personal data.
  • 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 Formspree. Formward's prices are on the plans page, and where each part of Formward runs is on EU data residency.

Step-by-step checklist

  1. Create the form. In the Formward dashboard, create a form, set the notification email and copy the endpoint.
  2. Change the action. Replace https://formspree.io/f/... with the Formward endpoint in the form's action or your fetch() call. The endpoint rewriter under Dashboard, Import & migrate does this from your Formspree URL.
  3. Rename the special fields. Rename subject to _subject. Leave the visitor's email field named email; it already sets Reply-To.
  4. Move the redirect. Add a hidden _redirect field pointing at your thank-you page, or set the redirect URL on the Formward form.
  5. 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.
  6. 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.
  7. Recreate plugins. Set up webhooks, Slack, Google Sheets and the autoresponder on the Formward form.
  8. Bring the history. If you want past submissions, export them as CSV from your old provider and import them under Dashboard, Import & migrate.
  9. Retire the Formspree form. Switch the live form, watch the first real submissions arrive, then disable the form at Formspree 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.

Migrate from Formspree to Formward