Spam and origins
Two checks run on every submission, and a third runs if you have bought it. The first two do different things to what they reject, and the difference matters: one throws the submission away before it is ever stored, the other keeps it and declines to deliver it.
The origin allowlist
Each form carries a list of origins it will accept submissions from. A submission whose origin is not on the list is discarded and never stored.
That is the stronger of the two outcomes, and it is chosen so a flood cannot amplify into write volume. Someone who has your form ID, which is in your page’s HTML and therefore public, can send you as many requests as the global throttle allows. What they cannot do is fill your database, because the check that stops them runs before the write.
An origin is a scheme, host and port: https://example.com. No path, no trailing slash, and
https://example.com and https://www.example.com are different entries. Remember preview
deployments and http://localhost:4321 if you test against the real endpoint.
Where the origin comes from depends on how the form was submitted:
- Name
Origin header- Type
- always for fetch
- Description
CORS requires it, so any JavaScript submission carries one.
- Name
Referer header- Type
- fallback, form posts only
- Description
A plain form post is a top-level navigation, and browsers have never agreed on whether those send an
Origin. Referer is consulted for form-encoded bodies only, and never for JSON.
- Name
neither- Type
- discarded
- Description
A known gap rather than an oversight. Accepting an unattributable body would let anyone holding a form ID force writes, and the thing that would make that safe is per-form rate limiting, which does not exist yet.
The honeypot
A honeypot is a text input people cannot see and bots fill in anyway. If it arrives non-empty, the submission is stored, marked as spam, and never delivered.
Unlike the origin check, this one is decided after the submission is safely stored, not before. The endpoint’s job is to get the lead durable and get out; judging it happens behind the queue. So a honeypot hit is a row that exists, carries a spam flag, and is never routed to a destination.
Stored rather than discarded, because the honeypot can be wrong. A password manager or an over-eager browser autofill can put something in it on behalf of a real person, and that person’s enquiry should still exist somewhere you can find it. It is in the console, flagged, rather than in your inbox.
The field name is per-form
The console generates a name for each form, something like client_reference or project-code, and it does not reuse one. Copy it verbatim into your markup.
A fixed, guessable name, the _gotcha that some services use, is worth very little: the field name is the only thing distinguishing the trap from a real input, so a bot that knows the name skips it and every form on the service is defeated at once. A name that reads like an ordinary business field, and differs per form, has to be defeated one form at a time.
Two names to avoid if you pick your own:
- Anything ubiquitous, such as
message,commentorsubject. Real forms have those fields, so bots fill them on purpose. - Anything a browser’s autofill heuristics recognise, such as
phone,addressororganization. Autofill will populate it for real people and you will mark their submissions as spam.
Hiding it correctly
<div style="position:absolute;left:-9999px" aria-hidden="true">
<label for="YOUR_HONEYPOT_FIELD">Company website</label>
<input
id="YOUR_HONEYPOT_FIELD"
name="YOUR_HONEYPOT_FIELD"
type="text"
tabindex="-1"
autocomplete="off"
/>
</div>
Five things are doing work there, and dropping any one of them costs you something:
- Nothing in the markup names the trap. The
idis the field’s own name rather than something likehp, the label reads like the field, and the wrapper is styled inline rather than with ahoneypotclass. A per-form name is the whole defence, and it is worth nothing if everything around it announces what the field is to anything that reads the page. - Off-screen, not
display: none. Some bots skip fields that are not displayed, which defeats the trap. aria-hidden="true"keeps it off a screen reader.tabindex="-1"keeps it out of the keyboard tab order, so nobody lands in it by accident.autocomplete="off"discourages the autofill that would otherwise mark real submissions as spam.
AI spam scoring
A paid add-on, off unless you buy it, and the only part of ezform that sends a submission’s text anywhere. It reads what the first two checks let through and asks one of two questions about each form. Which one is yours to pick, in the form’s settings.
Is this unsolicited? The default, on from the moment the add-on is active, with nothing to configure. A judgment about the sender: a bulk pitch, an SEO offer, a staffing agency, a backlink scheme. A submission judged unsolicited is stored, marked as spam, and never delivered, exactly as a honeypot hit is, and it sits in the console with the score that held it. One click restores it and sends the mail that would have gone out.
Is this what the form is for? The alternative, and it needs a sentence from you. Describe what the form is for and every submission is scored from 0 to 1 against that sentence. Nothing is held back by the score on its own. A roofer emailing a video production company for a quote is not spam by any reading, and is still not what that form is for.
One question per form, not both. Asked together, the two answers bend toward each other: a sentence describing what you want starts deciding what was asked for, and a category you welcome starts looking like a better fit. Separating them is what keeps a low score from quietly becoming a withheld lead.
Telling spam filtering what you welcome
Some forms exist to receive exactly what the list above refuses. A recruiter’s form wants recruitment approaches; an agency’s “pitch us” page wants pitches. In the form’s settings you can tick the categories that form welcomes, and approaches of that kind arrive like any other lead:
- Recruitment, staffing and hiring
- Agencies offering services or outsourced work
- SEO, link building and marketing services
- Investment offers and approaches from funds
This widens one category and nothing else. Everything unticked is judged exactly as it would be on any other form, and a welcomed approach that is still machine-generated filler is still spam.
It is a fixed list rather than a sentence you write, deliberately. This is the one setting that can push a submission towards delivery, so a vague one would quietly disable the filter you are paying for.
A low score does nothing until you say so
This is the important difference between the two. An unsolicited verdict withholds mail on its own. A relevance score never does. It is recorded and shown next to the submission, and the mail goes out exactly as it would have.
It withholds only when you write a routing rule that says it should, which is the trade you are making by choosing this question: nothing is ever held back that you did not ask to be. Rules address the score as #relevance, with the # marking it as something we worked out rather than something your visitor typed, so a form with a field genuinely named relevance keeps it and a bot cannot defeat the rule by posting one. The console offers a one-click rule for the common case, and what it writes is an ordinary route you can then reorder, edit or delete.
What it costs you, and when it stops
The add-on includes a monthly allowance of scored submissions, quoted on the pricing page. Past that, scoring stops and delivery does not: the submission is judged on the honeypot alone, delivered as usual, and recorded as unscored. The same is true if our model provider is unavailable. Nothing is ever held back because we could not score it.
What leaves, and what does not
The text of a scored submission is sent to TypeSafe AI, named in the privacy policy, to answer those two questions and nothing else. The model answers with a number and writes no text. Three limits are worth stating plainly:
- Nothing is sent for an account without the add-on. Free plans and paid plans without it are the same posture as before the feature existed.
- Nothing is sent for a submission the honeypot already caught. It is already withheld, so scoring it would change nothing.
- Only a capped excerpt of each field leaves, not the whole message, and the privacy policy says how much. Your stored submission is never truncated; the copy sent to be judged is.
What is not implemented
Worth being direct about, because these are the measures that do the real spam work elsewhere:
- No time-to-submit check.
- No nonce or signed form token.
- No per-IP or per-form rate limiting. The only throttle is global across the service.
- No sender reputation. Nothing here knows that an address or a domain has hit ten thousand other forms this week, and a model reading one message cannot infer it.
- No content scoring on the free path. That is the paid add-on above, and without it the honeypot is the only judgment made about a submission’s contents.
The honeypot plus the origin allowlist buys an interval, not a war. If you are being targeted specifically rather than sprayed, they will not be enough on their own, and scoring is a better reader of a targeted pitch than of a determined one.