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.

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, comment or subject. Real forms have those fields, so bots fill them on purpose.
  • Anything a browser’s autofill heuristics recognise, such as phone, address or organization. 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 id is the field’s own name rather than something like hp, the label reads like the field, and the wrapper is styled inline rather than with a honeypot class. 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.

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.