Migrating from Formspree to Formspring: a 5-minute guide
Switch from Formspree to Formspring without breaking your forms or webhook receivers. Real code, real steps, real timing.
Most teams who switch from Formspree to Formspring do it for one of three reasons: GDPR / EU data residency (governed by Regulation (EU) 2016/679), signed webhooks (industry-standard HMAC with the t=,v1= format), or AI-assisted submission moderation. The migration itself is one of the simplest backend swaps you'll do this year - usually under 5 minutes for the form, plus a one-line update to webhook receivers.
What changes
Three things change:
- The form's
actionURL - fromhttps://formspree.io/f/yourIDtohttps://formspring.io/f/abc123. - The webhook signature header (if you receive webhooks) - from Formspree's bespoke header to
X-Formspring-Signature: t=…,v1=…(industry-standard HMAC). - Where data lives - from US (Formspree) to the EU (Formspring).
Everything else stays the same. The body shape is nearly identical. Spam protection (honeypot, Akismet) runs by default. Email notifications work out of the box.
What doesn't change
- Your form's HTML structure. Same
<form>element, same fields, same button. - Your visitors' experience - they see the same form, submit the same way.
- Your CRM/Slack/Notion integrations if they're via Zapier (recreate the trigger, the rest is identical).
- Your dashboard ergonomics - both products show submissions in a list, both let you reply, both export CSV.
Step 1: create a Formspring account
Sign up free, no credit card. Verify your email. Create a new form (give it a name like "Contact" or "Newsletter").
Each form gets a unique endpoint URL like:
https://formspring.io/f/abc123XYZ
Copy it.
Step 2: swap the form action
Find your existing Formspree form:
<form action="https://formspree.io/f/yourID" method="POST">
<input type="email" name="email" required>
<textarea name="message" required></textarea>
<button>Send</button>
</form>
Change one attribute:
<form action="https://formspring.io/f/abc123" method="POST">
<input type="email" name="email" required>
<textarea name="message" required></textarea>
<button>Send</button>
</form>
That's the form migration. Push the change to production.
Step 3: test a real submission
Submit your form. Check the Formspring dashboard. Confirm the submission lands within 1-2 seconds. Check your email for the notification (default-on for paid plans).
If anything's off:
- Submission doesn't appear: check CORS settings. Add your site's origin in Formspring → Form → CORS.
- No email: verify your account email is confirmed.
- Spam-classified: Formspring's default spam threshold is conservative. Check the spam tab.
Step 4: update webhook receivers (if you have them)
This is the only non-trivial step, and it's still small.
Formspree's webhook signature uses a custom header. Formspring uses the industry-standard t=,v1= pattern:
X-Formspring-Signature: t=1715090123,v1=8e2c...sha256-hex
Verifying it in Node:
import { createHmac, timingSafeEqual } from 'crypto';
function verifyFormspringSignature(rawBody, headerValue, secret) {
const parts = headerValue.split(',').map(s => s.split('='));
const params = Object.fromEntries(parts);
const signedPayload = `${params.t}.${rawBody}`;
const expected = createHmac('sha256', secret).update(signedPayload).digest('hex');
return timingSafeEqual(Buffer.from(expected), Buffer.from(params.v1));
}
Same in PHP:
function verifyFormspringSignature(string $rawBody, string $headerValue, string $secret): bool {
$params = [];
foreach (explode(',', $headerValue) as $part) {
[$k, $v] = explode('=', $part, 2);
$params[$k] = $v;
}
$signedPayload = "{$params['t']}.{$rawBody}";
$expected = hash_hmac('sha256', $signedPayload, $secret);
return hash_equals($expected, $params['v1']);
}
Same in Python (hmac.compare_digest), Ruby (OpenSSL::HMAC with constant-time comparison), Go (hmac.Equal).
Replace your Formspree verification function. Test with a real webhook delivery. Done.
How do the two products compare feature-for-feature?
The matrix that captures what changes and what doesn't. Use it to confirm nothing material is missing before you cut over.
| Capability | Formspree | Formspring |
|---|---|---|
| Form action URL pattern | https://formspree.io/f/yourID |
https://formspring.io/f/abc123 |
| Default region | US | EU (Germany/Finland) |
| DPA included | Enterprise tier | Every paid plan |
| Spam protection | reCAPTCHA + Akismet | Honeypot + hCaptcha + Akismet + custom rules + AI moderation, layered |
| Visible captcha default | reCAPTCHA | hCaptcha |
| Webhook signature header | Vendor-specific | X-Formspring-Signature: t=…,v1=… (industry-standard) |
| File upload cap | 10 MB | 25 MB |
| File storage | Public-ish URLs | Private encrypted storage, signed-URL access only |
Magic fields (_subject, _redirect) |
Yes | No - dashboard configuration instead |
| AI moderation | No | Yes, default-on Pro+ |
| Multi-step funnels | No | Yes (separate product, same workspace) |
| Surveys | No | Yes (separate product, same workspace) |
| Branded short links | No | Yes |
| Free tier | 50 submissions/month | 50 submissions/month |
| Paid entry | $10/mo | $19/mo flat with everything |
The columns that drive most migrations: EU residency, signed webhooks, and the AI moderation layer. The columns that surprise teams after cutover: the magic-field rewrite (you configure email subject and redirect in the dashboard, not in HTML) and the multi-product workspace (the same account holds forms, surveys, funnels, and short links).
The full migration runbook
The five-minute version of this migration is real for a single contact form. A serious migration - multiple forms, webhook receivers in production, CRM integrations - benefits from a runbook. The shape:
1. Pre-migration audit
Before you change a single URL, inventory what you have:
- Every form on every page. Search the site for
formspree.io/f/. Record the form ID, the page it lives on, the destination email, the webhook destination, and the spam-protection settings. - Every webhook receiver pointed at Formspree. Note its language, its signature-verification function, and the secret it uses.
- Every third-party integration that listens to Formspree (Zapier, Make, n8n, custom). Note the trigger and any field mappings.
- The submission volume per form for the last 30 days. This becomes your baseline for verifying nothing breaks.
The audit takes an hour. Skipping it is how the migration eats a week.
2. Export historical submissions
Pull every form's submission history to CSV from the Formspree dashboard before the cutover. The export is not strictly required - Formspring will keep going forward fine without history - but a clean CSV per form gives the team a fallback if a downstream system (CRM, analytics) needs to reconcile.
Two reasons to export every form: archival continuity, and the regulatory case where a data subject asks for everything you hold about them. The agency or controller needs to be able to answer that question even after the vendor swap.
3. Stand up the new forms in parallel
Create the matching forms in Formspring. Do not touch the production site yet. For each new form:
- Set the same destination email.
- Set the same webhook URL.
- Configure the redirect URL (replacing the
_redirectmagic field if Formspree used one). - Configure the email subject (replacing the
_subjectmagic field if Formspree used one). - Test a submission from the dashboard's test panel. Confirm it lands at every destination.
You now have two parallel form backends, neither of which is touching production yet.
4. Dual-write phase
Update each form on the site to POST to both backends in parallel for a defined window - typically 5 to 14 days. The pattern: a single visible form posts to Formspring, while a tiny JavaScript shim duplicates the submission to Formspree. Both dashboards receive every submission. You watch both for:
- Submission counts (should match exactly).
- Spam-detection rate (different layers, expect divergence - track which lands in spam where).
- Email notification timing (Formspring typically arrives within 1-2 seconds; investigate any drift).
- Webhook delivery success rate.
A divergence in counts during this phase means the migration is not safe to cut over. Investigate before proceeding.
5. Cutover
Once dual-write has run cleanly for the chosen window, flip the production form's action to the Formspring URL and drop the dual-write shim. The cutover itself is a one-line change. The risk has already been retired by the dual-write phase.
Cut over one form at a time, not all of them at once. If something breaks, the blast radius is one form.
6. Webhook receiver swap
Update every webhook receiver to verify the Formspring signature instead of the Formspree one. Deploy the receiver, then point the Formspring webhook at it. Verify the receiver processes a real delivery correctly. Replay a recent webhook from the Formspring dashboard to confirm idempotency-key handling works - see webhook idempotency: handling duplicate deliveries for the pattern.
If the receiver has to support both signatures during a transitional window (because some forms have cut over and some have not), have it try Formspring's verification first and fall back to Formspree's only if the header is missing. The fallback path can be removed once every form has cut over.
7. Rollback plan
The rollback is the same as the cutover, reversed: change the form's action back to the Formspree URL. The dual-write shim from the migration window is still in your git history; reinstate it if the rollback needs to be reversible mid-week.
A rollback that has been planned takes 15 minutes. A rollback that has not been planned takes a day. Write the rollback steps in the migration ticket before you start the cutover.
8. Decommission
After 14 days of clean operation on Formspring with no rollback events, decommission Formspree. Cancel the subscription. Archive the exported CSVs. Remove the dormant Formspree credentials from password managers and secret stores. Note the decommission date in the audit doc - auditors ask.
What pitfalls catch teams during the migration?
The recurring failure modes worth knowing in advance.
DNS on custom domains. If you were serving the Formspree form via a vanity subdomain (forms.yourbrand.com → Formspree's CDN), the CNAME needs to flip to Formspring's host during cutover. DNS propagation is not instant. Plan for a window where the old domain still resolves to Formspree for some visitors and to Formspring for others. The fix: keep both endpoints accepting submissions until the CNAME has fully propagated, which is the dual-write phase doing its job.
Webhook secret rotation. The Formspring HMAC secret is different from the Formspree one. Rotating into the new secret on the receiver requires updating an environment variable and redeploying. Teams that update the secret in production without redeploying the receiver see every webhook fail signature verification for the window between update and deploy. Do the secret rotation as a coordinated, scripted step.
Third-party integrations that need re-authorisation. Zapier, Make, n8n, custom CRM webhooks, marketing-automation triggers, analytics event-forwarders - every integration that authenticated to Formspree needs to authenticate to Formspring afresh. The integrations do not migrate; only the form does. Build a checklist of every integration touched and check off the re-auth one at a time.
The magic-field translation. A form that relied on _subject or _redirect to control email subject and post-submit redirect needs both moved into Formspring's dashboard configuration. Forgetting one means submissions land but the autoresponder uses a default subject, or the visitor sees a JSON response instead of a thank-you page. The migration is not done until every magic field has been translated.
Spam-folder false positives during the dual-write window. The two backends use different spam layers. A legitimate submission that lands in the inbox on one and the spam folder on the other will look like a count mismatch until someone checks both spam folders. Build "check spam folders on both sides" into the daily review during dual-write.
File-upload size regression in reverse. If a form was capped at Formspree's 10 MB and you tighten the cap during migration, visitors who routinely uploaded larger files get rejected. Going the other way (10 MB → 25 MB on Formspring) is free; going down requires a heads-up to anyone with a saved bookmark to the form.
Analytics event continuity. A "form submitted" GA4 / Plausible / Fathom event tied to a Formspree-specific redirect URL needs the redirect to match. If the post-submit URL changes, the analytics funnel resets and the historical conversion-rate comparison breaks. Keep the redirect URL identical across the swap.
CORS allowlist on the new backend. Every site that POSTs to the form needs to be in the Formspring CORS allowlist. Forgetting a subdomain (staging.example.com, preview.example.com, a marketing-microsite host) causes the submission to fail silently in the browser while looking fine in production. Add every origin during setup, not after the first incident.
Step 5: run both in parallel for a week (optional, recommended)
Don't disable Formspree the same day. Point one copy of your form at Formspring, keep the production copy on Formspree, and watch both dashboards for a week. Compare:
- Submission counts (should match).
- Spam detection rate.
- Email notification timing.
- Webhook delivery success rate.
If everything checks out after 7 days, swap the production form. If something's off, you have time to debug without losing leads.
Step 6: decommission Formspree
Cancel the Formspree subscription (if any) at the end of your billing period. Export your historical submissions to CSV from Formspree's dashboard for record-keeping.
Formspring won't auto-import old Formspree data - by design, for privacy. If you need historical continuity, use the Formspring API to push your CSV in.
Common questions
Will my Zapier connection still work?
Recreate it on Formspring's Zapier app. The triggers and shape are nearly identical, so it's a 10-minute job.
Can I use the same form ID?
No - each form backend assigns its own opaque IDs. Formspring's IDs are 8-16 character strings (like abc123XYZ). Update the URL in your HTML.
What about the Formspree-specific reCAPTCHA?
Formspring uses hCaptcha by default (free, accessible, GDPR-friendly). It's drop-in via a meta config; no additional code needed.
Will my form's _subject field still work?
Formspring doesn't use Formspree's underscore-prefixed magic fields. Configure email subject in the dashboard instead. All visible fields submit normally.
My form has _redirect for thank-you pages. Equivalent in Formspring?
Configure the redirect URL in Formspring → Form → Redirect after submission. The user-facing experience is identical; the configuration moved from form HTML to the dashboard.
Why teams do this migration
- GDPR + EU residency: Formspree is US-hosted; that's the most common driver, especially after the Schrems II ruling (C-311/18) raised the bar on EU→US transfers.
- Signed webhooks: industry-standard HMAC works with every existing receiver, off the shelf.
- AI moderation: catches edge cases that signature-based spam detection misses.
- Honest pricing: Pro is $19/mo flat with everything included; no enterprise upsell for the DPA.
Related from this desk
- GDPR-compliant form submissions: what you need to know - the compliance ground that justifies most Formspree migrations in the first place.
- EU-only form hosting: why it matters in 2026 - why EU residency keeps showing up at the top of the migration checklist.
- Verify HMAC webhook signatures in Node, PHP, and Python - what your existing webhook receivers need to keep working after the cutover.
- Why webhook deliveries fail and how to replay them safely - replay tooling that the migrated platform should give you on day one.
- Product side: form backend.
Try Formspring free
50 submissions/month free, no credit card. Sign up and have your first form running in 5 minutes.
Written by Florian Wartner
Florian Wartner
Founder of Formspring and Pixel & Process. Senior full-stack engineer based in Lübeck, Germany. Building developer-first SaaS with EU data residency and honest pricing.
Elsewhere