Formspree and WebForms share a shape: an HTML form posts to an endpoint URL, and the service takes it from there. That makes the mechanical part of a migration small - one action attribute, two field renames. The interesting parts are behavioral: where Formspree configures things in its dashboard, WebForms usually accepts a field in the form, and one feature that is paid on Formspree is free here. This guide covers both kinds of difference, verified against Formspree's live documentation on September 3, 2026.
The endpoint swap
<form action="https://formspree.io/f/YOUR_FORM_ID" method="POST"> <input type="email" name="email" required> <textarea name="message" required></textarea> <button type="submit">Send</button> </form>
<form action="https://send.webforms.to/wft_your_key" method="POST"> <input type="email" name="email" required> <textarea name="message" required></textarea> <button type="submit">Send</button> </form>
That form is already fully migrated. Both services treat a field named email as the reply-to address automatically, so replying to the notification reaches the person who submitted the form - no configuration on either side. Your other data fields carry over untouched; neither service imposes a schema.
The control-field mapping
- email -> email: unchanged. Both services use it as the notification's reply-to automatically.
- subject -> _subject: same job, underscore prefix. The prefix is deliberate - a directive can never collide with a real data field called subject.
- _gotcha -> _gotcha: identical, honestly. Both services silently discard a submission whose hidden _gotcha field arrives filled. Your existing honeypot markup works without edits.
- Thank-you redirect -> _redirect: on Formspree this is a dashboard setting on paid plans. Here it is a hidden field in the form, on every plan including free.
One Formspree feature has no field equivalent here: subject templating with {{ variable }} interpolation. Our _subject is a literal string. If you were composing subjects out of submission values, set the subject in JavaScript before submitting - the AJAX section below shows where it goes.
The redirect: from dashboard setting to form field
Formspree's thank-you redirect lives in the form's Settings tab and is available on its paid plans. WebForms takes it as markup: a hidden _redirect input holding an absolute https URL, no plan gate. If your form currently shows Formspree's hosted thank-you page, you can keep that behavior by simply not setting _redirect - ours renders a confirmation page too, and Pro workspaces can retitle it.
<input type="hidden" name="_redirect" value="https://yoursite.com/thanks.html">
AJAX and framework forms
If you use @formspree/ajax or the React library, the replacement is a plain fetch - there is no client library to install because none is needed. POST the FormData to your endpoint with an Accept: application/json header and branch on the ok field of the response:
const res = await fetch("https://send.webforms.to/wft_your_key", {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
});
const data = await res.json();
if (data.ok) {
// show success
} else {
// data.error is a stable machine-readable code
}This is also where a computed subject goes, replacing {{ name }} templating: formData.set("_subject", `Message from ${name}`) before the fetch.
What changes behind the same POST
Formspree delivers to email and its integrations; WebForms fans one submission out to email, Slack, Discord, Telegram and webhooks simultaneously, stores every submission searchably, and logs each delivery attempt per destination with its status and response. The free plan takes 300 submissions a month - six times Formspree's free 50, per their pricing page as of August 2026 - with spam filtering that layers a honeypot, a time trap, disposable-email blocking and heuristics rather than leaning on reCAPTCHA alone.
The checklist
- Create a form in the dashboard and copy its endpoint.
- Swap the action URL.
- Rename subject to _subject if you set one; email and _gotcha stay as they are.
- Move a dashboard-configured redirect into a hidden _redirect field.
- Replace @formspree/ajax with a plain fetch if you used it.
- Submit once for real; confirm it in the dashboard and the delivery log.