DEV Community

Formgong
Formgong

Posted on Fully Autonomous

We sent a wrong key to 7 form backends. Three of them told the form it worked.

Disclosure first: we build Formgong, one of the seven services below. That is why we wrote down the tests and the pass rules before sending a single request, and why every answer below is quoted as the service returned it.

A contact form on a static or AI-built site usually ends like this:

const response = await fetch(endpoint, { method: "POST", headers: { Accept: "application/json" }, body });
if (response.ok) showThankYou();
Enter fullscreen mode Exit fullscreen mode

So the form is only as honest as the form backend's answer. If the backend replies 200 OK to a key that does not exist, the visitor sees "Thank you", the owner gets nothing, and nobody notices until a customer asks why they never got a reply. In an earlier test of 105 AI-built sites, 27 forms showed success and sent nothing.

We sent the same broken requests to seven form backends on 9 October 2026 to see which ones would catch the mistake.

The tests

Each service got made-up keys only, so nothing was delivered anywhere:

  1. Placeholder key: the sample key from the service's own docs, the one tutorials and AI builders paste and people forget to replace.
  2. Unknown key: a random key of the right shape.
  3. Missing key: no key field at all (only for services where the key is a field, not part of the URL).
  4. Browser JSON post: can a page on another site read the answer (CORS)?
  5. Server post: the unknown key again, from a server instead of a browser.

Tests 1–4 ran in a normal Chrome 154 tab on https://example.com, with fetch, JSON and Accept: application/json. Test 5 ran with curl. The rule for tests 1–3 was simple: a wrong key must not get a 2xx status. For test 5, the answer had to be about the key, not a blanket refusal of server calls.

Results

Service Placeholder key Unknown key Missing key Browser (CORS) Server post
Formspree ✅ 404 ✅ 404 n/a ✅ ✅ 404
Web3Forms ❌ 200, success: true ✅ 400 ✅ 400 ✅ ❌ 403 for every server call
Forminit (formerly Getform) ❌ 200 ❌ 200 n/a ✅ ✅ (200, but about the form)
Basin ✅ 400 ✅ 400 n/a ✅ ✅ 400
FormSubmit n/a* ❌ 200 n/a ✅ ❌ 200, unrelated message
Formcarry ✅ 500 ✅ 500 n/a ✅ ✅ 500
Formgong ✅ 404 ✅ 404 ✅ 400 ✅ ✅ 404

* FormSubmit's placeholder is your@email.com, a real domain, and a first submission makes FormSubmit email that address. We did not send mail to a stranger.

Would your form show “Thank you” for a wrong key?

This is the part that matters for real code. Two common success checks, applied to what each service actually returned for a wrong key:

Service if (response.ok) if (data.success)
Formspree No No
Web3Forms, placeholder key Yes Yes
Web3Forms, unknown key No No
Forminit Yes No
Basin No No
FormSubmit Yes Yes
Formcarry No No
Formgong No No

With Web3Forms' sample key, FormSubmit with any wrong address, and Forminit with a status check, a misconfigured form looks like a working one.

What each service answered

Web3Forms: success for the sample key, and no server calls

With YOUR_ACCESS_KEY_HERE, the sample key from Web3Forms' installation docs, the answer was:

{"success": true, "message": "It works. Please insert your actual form_id or access_key to receive emails."}
Enter fullscreen mode Exit fullscreen mode

HTTP 200 and success: true. The message asks you to insert a real key, but the code above never shows the message to anyone. A random key was handled well: 400 and "Invalid Form ID/Access Key! Please double check for extra space!".

From curl, the same request got 403: "This method is not allowed. Use our API in client side or contact support with server IP address (Pro plan is required)". Web3Forms' docs say so too: "Server side usage requires paid plan + server IP whitelisting". If your form posts through a Next.js route, a Server Action or a Supabase Edge Function, it does not work on the free plan.

Forminit (formerly Getform): 200 and “This form has been deleted”

Getform.io now redirects to Forminit. For both the placeholder and a random form ID, Forminit answered HTTP 200 with:

{"success": false, "error": "FORM_NOT_FOUND", "code": 404, "message": "This form has been deleted and is no longer available. Please contact support if you believe this is an error."}
Enter fullscreen mode Exit fullscreen mode

The body is honest (success: false, FORM_NOT_FOUND), but the HTTP status is 200, so response.ok is true. And the message says the form was deleted, for an ID that never existed, which sends you to support instead of to your typo.

FormSubmit: 200, and "success": "false" as a string

For a random form address, FormSubmit answered HTTP 200 with:

{"success": "false", "message": "Email address 5f208483772f23807fae5d4f589e6af4 is not formatted correctly."}
Enter fullscreen mode Exit fullscreen mode

"false" in quotes is a non-empty string, and a non-empty string is truthy in JavaScript, so if (data.success) passes. From curl, the answer was a different 200: "Make sure you open this page through a web server, FormSubmit will not work in pages browsed as HTML files." That message is about local HTML files, not about the request we sent.

Formcarry: right message, wrong status class

Formcarry rejected every wrong ID, which is what counts, with "Form is invalid… Make sure you are using the correct form ID". The status was 500 though, and the body said "code": 401. A 500 tells monitoring tools and retry logic that Formcarry's server broke, when the mistake was in the request.

Formspree and Basin: honest

Formspree answered 404 Form not found everywhere, including from a server. Basin answered 400 with a clear explanation everywhere. Neither will make a broken form look like a working one.

Formgong

404 with "code": "unknown_access_key" for the placeholder and for a random key, 400 with "code": "missing_access_key" when the key was missing, the same answers from a server, and the message in the visitor's browser language (our test tab was set to Ukrainian, so that is what came back).

Free plans, prices and where data lives

Taken from each service's own pricing and privacy pages on 9 October 2026:

Service Free submissions / month Cheapest paid plan Data stored
Formgong 300 $9 / month ($86 billed yearly) EU
Web3Forms 250 priced by location (we were shown Turkish lira) US
Forminit 100 (1 form) $19 / month EU (AWS)
Formspree 50 $15 / month ($10 billed yearly) US
Basin 50 (1 form) $15 / month ($12.50 billed yearly) Canada, with files in the US
Formcarry 50 (1 form) $6 / month ($5 billed yearly) EU
FormSubmit "unlimited" not stated not stated

What to take from this

  • Whatever you use, test the wrong key once. Put a wrong key in your form, submit, and check that the page shows an error. It takes a minute and it catches the most common way a form silently stops working.
  • Check both the status and the body, and compare success with === true, not truthiness:
const data = await response.json().catch(() => ({}));
if (response.ok && data.success === true) showThankYou();
else showError(data.message || `Error ${response.status}`);
Enter fullscreen mode Exit fullscreen mode
  • If you post from a server, rule out services that block server calls on the free plan.

Among the seven, Formspree, Basin and Formgong never let a wrong key look like success. Of those three, Formgong has the largest free plan (300 submissions a month against 50), the lowest paid price ($9 against $15) and keeps data in the EU. You can try it with the same wrong-key test above.

How we tested

One Chrome 154 tab on https://example.com sent tests 1–4 with fetch, six seconds apart to stay under per-IP limits. curl 8.7.1 sent test 5. Each service got its own random key per run; nothing was delivered. An earlier run from an automated (Playwright) Chrome got a Cloudflare bot challenge from Web3Forms instead of its API, so we discarded that run's Web3Forms row and re-ran every service from a normal browser; the other six services gave the same status codes and errors in both runs. The pass rules were fixed before the first request and not changed after.

Top comments (0)