Skip to content

Spam protection

Six layers, from free and invisible to strict. Start with the honeypot; add the others as a form needs them.

Every form has a honeypot: a field that real visitors never see and bots fill in. Any value in it marks the submission as spam. The submission is kept in the form’s Spam folder for 30 days, nobody is emailed, no webhook fires and it doesn’t count toward your quota. The bot gets the normal success response, so it can’t tell it was caught.

The field is called fax until you rename it (the form’s Settings, Spam & security). Pick something a bot would want to fill in and keep the name in sync with your markup; the code on the Fields tab always uses the current name. The field is stripped before storage, so it never shows up in emails, the dashboard, the API or exports.

The honeypot input
<!-- Off-screen, never focused, never autofilled. Bots fill it; people don't. -->
<input
type="text"
name="fax"
tabindex="-1"
autocomplete="off"
aria-hidden="true"
style="position:absolute;left:-9999px"
/>

Forms moved from Web3Forms keep working: a botcheck field is treated as a honeypot too, with false and 0 counting as empty.

With the filter on (it is by default), each submission gets a spam score from what it contains and where it came from: no readable text, many links or link markup, short or risky links, common spam phrases, gibberish, shouting, repeated text, a throwaway email address, no browser or no page behind the post, and bursts or repeats from one sender. A high score sends it to Spam. Strict filtering moves borderline submissions too. Turn the filter off on a form whose real messages are link-heavy. Spam is kept for 30 days whatever the form’s retention, so a wrongly filed message can be moved back to the inbox from the dashboard.

List words or phrases under Settings, Spam & security, and any submission that contains one goes to Spam. Matching ignores case.

The endpoint accepts 30 posts a minute from one IP address and 120 a minute to one access key; past that it answers 429 rate_limited. Your plan’s submissions a month are a soft quota, so a busy month never drops a real message, and an abuse ceiling far above it stops a runaway script. Spam has its own ceiling and never eats into your quota.

A form can use hCaptcha with Formwire’s own site key, so there is nothing to sign up for, or, on Pro, Cloudflare Turnstile or Google reCAPTCHA (v2 or v3) with your own keys. Pick one under Settings, Spam & security, then add that widget to the form. The widget posts its token in h-captcha-response, cf-turnstile-response or g-recaptcha-response, which Formwire verifies and drops.

  1. In the form’s Settings, Spam & security, set Captcha to hCaptcha and save.
  2. Copy the form’s code from its Fields tab again: it now has the widget in it. Or add these two lines to the form you already have, the widget inside the <form> just above the submit button:
hCaptcha with Formwire's site key
<div class="h-captcha" data-sitekey="9cb274e8-6782-4ebc-a8d6-4ca8f9b6646e"></div>
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>

The widget works on any domain, and the token it adds to the form is verified once and then dropped, so it never shows up with the submission. In React, use @hcaptcha/react-hcaptcha (<HCaptcha sitekey="9cb274e8-6782-4ebc-a8d6-4ca8f9b6646e" />), and in Vue @hcaptcha/vue3-hcaptcha, inside the form; the Fields tab’s React and Vue code does this for you. If you post with fetch, call hcaptcha.reset() after each try, since a solved captcha can only be used once.

A post with a missing or failed token goes to Spam with the reason captcha, and the sender gets the normal success response, like any caught bot. The widget is what stops a real person sending before they’ve solved it.

Your access key is public, so anyone can copy it into their own page. Allowed domains (Pro) stop that: list the hosts your form lives on and posts from any other page are refused with 403 domain_not_allowed. The check reads the browser’s Origin header, falling back to Referer.

  • example.com allows that host exactly. *.example.com allows any of its subdomains.
  • Paste a full URL and it’s cut to its host. Up to 50 entries per form.
  • Strict mode (on by default once you’ve listed domains) also refuses posts that name no site at all, such as curl, scripts and server-side code. Turn it off if a backend posts to the form as well as your pages.
  • The dashboard’s test submission has no page to come from and is always accepted.

The form’s spam settings show the sites that were blocked, and the API’s GET /forms/{form_id}/origins (the list_form_origins MCP tool) lists every host that has posted with how many were accepted and blocked, so you can see what to allow before switching the list on.

Spam is stored in the form’s Spam folder with the reason it was caught: honeypot, captcha, filter or reported, with the filter’s signals. Nothing in that folder is emailed, sent to a webhook or counted. Move a message to the inbox if it was misfiled; it’s deleted for good after 30 days.