Spam protection
The FormParcel bot check runs on every form: one script line, no cookies, no widget. Plus the Spam folder, a trap field, rate limits and optional Turnstile.
What it does
Every form has these, with nothing to turn on:
- FormParcel’s bot check. A small script,
formparcel.js, does a short piece of work in the visitor’s browser and adds a proof to the form. A submission without a valid proof is not delivered. It waits in the Spam folder. - The Spam folder. Submissions that look automated or repeated are stored there instead of being delivered. You can release a real lead with Not spam.
- The trap field. A hidden field that people never see and bots fill in. A post that fills it is dropped.
- Rate limits and sender rules. Limits on how fast one sender can post, and rules for repeated and duplicate messages.
- A flood alert. When a form gets far more submissions than usual, the bot check gets harder and the dashboard tells you.
On top of that, a client site can turn on Cloudflare Turnstile with that client’s own free keys. It is optional: see Turnstile.
The bot check
The bot check is on for every form. The only setup is one script line on the page with the form. The code in your dashboard already includes it, and so does every example in these docs.
Example form address. Use yours from the dashboard.
<script src="https://f.formparcel.live/f/formparcel.js" async></script>The script is served from the same host as your form address, at /f/formparcel.js. It sets no cookies, stores nothing in the browser, shows no widget and loads nothing from a third party. Its only request is for a challenge, to the host the form already posts to.
How it works
- The script finds each form on the page whose
actionis a FormParcel form address on that host. - It asks FormParcel for a signed challenge for that form.
- In the background, it finds a number that makes the challenge’s SHA-256 hash start with 16 zero bits. That takes about 65,000 hashes, well under a second on a phone. The page stays responsive while it works.
- It adds the result to the form as the hidden field
_fp_proof. FormParcel checks it and never saves it with the lead.
FormParcel accepts a proof only when all of these hold:
- It was made for this form, and the challenge was signed by FormParcel.
- The challenge is less than 30 minutes old. The script fetches a fresh one before that, so a page left open keeps working.
- At least 2 seconds have passed since the challenge was issued. A person filling in a form takes longer than that. If a visitor sends sooner, the script waits until the proof is old enough, then sends.
- The challenge has not been used before. Each challenge works for one submission.
Sending with fetch
A page that sends leads with fetch() loads the same script and asks it for a proof before each send:
const _fp_proof = await window.FormParcel?.proof("https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE").catch(() => undefined);
// Send it with the other fields: JSON.stringify({ ...data, _fp_proof })If the script did not load, proof is not there and the lead is sent without a proof. It is still saved, and it waits in the Spam folder. The full sample is in the fetch guide.
Without a valid proof
A submission without a valid proof is not refused, because a real visitor whose browser blocked the script should not lose their message. It is stored in the Spam folder: not delivered and not counted toward your plan. The receipt says Held as spam and gives the reason:
- Sent without FormParcel's bot check (formparcel.js). The post carried no proof. Usually the page is missing the script line.
- Failed FormParcel's bot check. The proof was there but did not pass: it was for another form, expired, made too fast or did not have enough work.
Release a real lead with Not spam. It is then counted and delivered. The visitor and the sending script get the same answer a lead gets, so a bot learns nothing it could tune against. The dashboard shows Bot check not installed for the form under Needs action.
Only a visitor’s browser can make a proof. A post from your server, a site builder’s webhook or curl never carries one, so its leads always wait here. See Bot check.
A proof used a second time is a replay. That post is held in the Spam folder too, never dropped: a visitor who sends again after an error sends the same proof, and their lead must not be lost.
Is it installed
The form’s Spam & security page shows the bot check’s state from the last 7 days of submissions:
- Bot check working. Submissions came with the bot check. Nothing to do.
- Bot check not installed. Submissions came without it. The page shows the script line to add.
- Bot check missing on some submissions. Some came with it and some without. A page with the form may be missing the script, or a visitor’s browser blocked it.
- No recent submissions. Nothing to tell yet.
Sites with a Content-Security-Policy
If the client’s site sends a Content-Security-Policy header, it must allow the form endpoint host, https://f.formparcel.live in this example, in both script-src (to load the script) and connect-src (to fetch the challenge). Otherwise the browser blocks the script and every lead waits in the Spam folder.
Test leads and Turnstile
A lead sent with Send a test lead from the dashboard skips the bot check. A post that passed the site’s Turnstile check does not need a proof either. Keep the script on a Turnstile site anyway: when Cloudflare cannot answer, the lead is accepted without the Turnstile check, and then the proof decides whether it is delivered or held.
The Spam folder
Besides a missing or failed bot check, these put a submission in the Spam folder: a third submission from one sender to a form in 24 hours, the same message twice in 24 hours, 3 or more links, or link markup. A held submission is stored encrypted like a lead, is never delivered and never counted, and is deleted after 30 days. Spam is not counted toward your limit.
More than 10 submissions from one sender to your workspace in a day are held as well.
Choose Not spam to release a real lead. Choose Mark as spam on a lead that got through: nothing more is delivered for it, and for 30 days every submission from that sender to your workspace goes to the Spam folder instead of being delivered. It is held, not refused, because many real people can share one network address, on a mobile network or in an office. A lead that was already delivered still counts toward your plan; one that was not gives its count back.
The Spam folder holds up to ten times your monthly lead limit each month. Once it is full, a submission that would be held is refused instead, with status 429 and the code SPAM_FOLDER_FULL, and the visitor is told the form cannot take more messages. The dashboard tells you. Leads that pass every check are still accepted.
The trap field
Every form has a hidden trap field, _gotcha by default. Keep it in the form, hidden from people. A post that fills it is dropped: nothing is saved and no receipt is created. How each kind of request is answered is on special fields.
Rate limits
Each form has limits on how fast one sender can post, and on how many leads a form and a workspace accept. Past a limit, a submission is refused with 429, except one: more than 10 submissions from one sender to a workspace in a day are held in the Spam folder, not refused, because an office or a mobile network can put many people on one address. The numbers are on Limits and retention. The challenge the script fetches is limited too: 30 a minute per sender, far more than a page needs.
Flood alert
A form counts as flooded when, within an hour, it holds or drops 30 submissions, or accepts more than 5 times its usual hourly leads (checked once it has 50 or more in that hour). For the next hour:
- The bot check gets harder: 20 zero bits, about a million hashes, a few seconds in a browser. Real visitors can still send.
- The dashboard shows “form name is being flooded.” with a link to the form, where you can pause it if the flood goes on.
No email is sent for a flood yet. You see it when you open the dashboard.
What this does not stop
- The allowed-origins list does not stop scripts. A browser sends an honest
Originheader, but a script can send any value it likes. The list keeps other websites from posting your form from their pages. It is not a bot check. - A determined attacker can get through. Someone who runs real browsers, solves every check and sends slowly can still post. The bot check makes each submission cost them computing time. The Spam folder, the sender and duplicate rules, and Mark as spam handle what remains.
- Junk already in a Sheet stays there. FormParcel does not delete rows it has written. Every row carries its submission ID in column A, so you can find a junk lead’s row by its receipt ID and delete it.
Turnstile: an optional extra layer
For a stronger check on top of the bot check, you can turn on Cloudflare Turnstile for a client site. A visitor passes a quick check before the form sends. A visitor who does not pass is refused, never becomes a lead and never gets a receipt.
- It is optional and set per client site, so it covers every form on that site. It is off until you turn it on.
- It uses the client’s own Cloudflare keys, not a shared FormParcel widget. The secret key is saved encrypted and is never shown again.
- The auto-reply needs it, because that email goes to an address a stranger typed.
Create a free widget for the client’s domain
- In the Cloudflare dashboard, open Turnstile and choose Add widget. A free Cloudflare account is enough.
- Add the client’s domain as the hostname, for example
client.example. A hostname also covers its subdomains. - Choose the Managed widget mode and create the widget.
- In FormParcel, open the client site and choose Edit site. Paste the site key and the secret key, turn on Check visitors with Turnstile and save.
- Copy the updated code from the form’s Setup tab into the client’s site. It now holds the widget as well as the bot check.
Why one widget per client. A free Turnstile widget covers up to 10 hostnames and takes no wildcards, so a single widget cannot serve many clients. Making one per client site also keeps each client’s keys, and their Cloudflare account, their own.
Which sites are accepted. A check passes only when the widget was served from the site’s domain or from a host in the form’s allowed origins, including their subdomains. If you test from another host, such as a development address, add it to the allowed origins.
What the form needs
The widget adds a field named cf-turnstile-response to the form. FormParcel reads it, checks it with Cloudflare and never saves it with the lead. The code in your dashboard already includes the widget while the check is on. This is the idea in a plain HTML form:
<form action="https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE" method="POST">
<!-- Your fields, the _gotcha trap and _next stay as they are. -->
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
<!-- Spam check (Cloudflare Turnstile): the widget adds its token to this form as cf-turnstile-response. -->
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">Send</button>
</form>
<!-- FormParcel bot check: no cookies, no widget. Without it, submissions wait in your Spam folder. -->
<script src="https://f.formparcel.live/f/formparcel.js" async></script>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>From a script, send the token as the cf-turnstile-response field in the JSON body, and ask the widget for a fresh one after every send, because a token works once:
// Load https://challenges.cloudflare.com/turnstile/v0/api.js once, and put
// <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div> on the page with your form.
// Keep FormParcel's bot check too (no cookies, no widget):
// <script src="https://f.formparcel.live/f/formparcel.js" async></script>
async function submitLead(data) {
const fp = window.FormParcel || await new Promise((ok) => { addEventListener("formparcel:ready", () => ok(window.FormParcel), { once: true }); setTimeout(ok, 10000); });
const _fp_proof = await fp?.proof("https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE").catch(() => undefined);
const response = await fetch("https://f.formparcel.live/f/f_k7QmXp2aVb9sTn4cLw8RhE", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ ...data, _fp_proof, "cf-turnstile-response": window.turnstile?.getResponse() }),
});
window.turnstile?.reset(); // a token works once: ask the widget for a fresh check
const result = await response.json();
if (response.status === 403 && result.code === "SPAM_CHECK_FAILED") {
// The visitor did not pass the check. Ask them to complete it and send again.
}
return result;
}What visitors see
A visitor who has not passed the check (no token, or a token that is invalid, expired, already used or made on another site) is refused with status 403 and the code SPAM_CHECK_FAILED. A script gets this JSON:
{
"success": false,
"error": "Please complete the spam check and try again.",
"code": "SPAM_CHECK_FAILED"
}A browser form post gets a short page titled “Please complete the spam check and try again”, which says the message was not sent and to go back, complete the check and send it again. Nothing was saved, so there is no receipt. Spam is not counted toward your limit. FormParcel counts these refusals per form and shows the count in the form’s settings.
If Cloudflare is down or the key is wrong
A real lead is never lost to a spam check. When the check itself cannot be made, the lead is accepted without it:
- Cloudflare does not answer within 3 seconds, or answers with a server error.
- Cloudflare rejects the site’s secret key, or FormParcel can no longer read the key it saved.
The lead’s receipt then says the spam check could not be made, and the bot check applies to it as it does on a site without Turnstile. If the cause is the secret key, the dashboard shows The spam check is not working on the site and the form, and says to paste the secret key again. While the check is not working, the auto-reply sends nothing, because it only goes to visitors who passed a real check.
Test leads
A lead sent with Send a test lead from the dashboard skips the Turnstile check and the bot check, so you can test a form without the widget or the script.