Requirements
- WordPress 6.9 or later (tested up to 7.1).
- PHP 7.4 or later. PHP 8.2 or later is recommended.
- HTTP or HTTPS, with nothing to set up on your server. HTTPS is still worth adding when you can, because it protects what visitors type.
- First-party cookies and JavaScript in the visitor's browser.
- One of the supported form plugins, or a form you built yourself: Contact Form 7, WPForms, Gravity Forms, Fluent Forms, Elementor Pro.
Multisite and network-wide activation are not supported. WordPress login, comments, WooCommerce and external endpoints are not protected automatically.
Install and protect your first form
- Test on a staging site before using Form Armour on a live site. A staging site is a private copy of your site, so a problem with a form cannot affect real enquiries.
- In WordPress, go to Plugins, Add New, Upload Plugin, choose the Form Armour zip and activate it.
- Open Form Armour, Forms. Your forms are listed in one card per form plugin.
- Set a form's protection to Local and click Save. The slider appears on that form straight away; you do not add a field in the form editor.
- Choose whether to turn on Block pasting for that form (see Troubleshooting before you do).
- Clear any page cache, open the page and send yourself a test message.
The Form Armour menu has five screens: Dashboard, Forms, Appearance, Stats and Settings. Every screen has a Help tab in the top right with a quick start, custom-form steps and troubleshooting notes.
Forms you have not chosen stay off. Discovery scans up to 500 records per form plugin. If a form is missing, use Add a form on the Forms screen to register it by its ID.
Form plugins
Contact Form 7
Form Armour hooks into Contact Form 7's spam check and adds a safeguard before mail is sent. When a submission is blocked, Contact Form 7 may show its own generic error rather than your Form Armour message.
WPForms
Standard WPForms forms are protected through its rendering and validation hooks. Conversational forms, uploads, payments and registration add-ons need testing on a staging site first.
Gravity Forms
Each validation step is checked, including page changes in multi-page forms and API validation. Entries inserted directly by privileged code are outside its scope. Test multi-page, payment and API workflows on staging.
Fluent Forms
Standard Fluent Forms rendering and validation are protected. Conversational forms and advanced add-ons need testing first.
Elementor Pro
The Elementor Pro Form widget is supported, including forms in pop-ups and saved templates. Third-party Elementor form widgets are not covered. Elementor forms are identified as post-ID:widget-ID if you register one by hand.
Custom forms
A form you built yourself needs two things: the slider inside the form, and a check in the code that handles the submission. The slider alone does not protect anything.
- On the Forms screen, under Add a form, register the form with the provider Custom forms and a slug such as
contact-enquiry. It is set to Local protection. - Render the slider inside your
<form>, before its submit button, with either of these:[form_armour id="contact-enquiry"]<?php echo form_armour_render('contact-enquiry'); ?> - In your submission handler, stop when validation fails, before sending email, saving, uploading or charging anything:
// After your normal method, authorisation and CSRF checks, // and before any side effects: if (!function_exists('form_armour_validate') || !form_armour_validate('contact-enquiry')) { wp_send_json_error(['message' => 'Please complete verification again.'], 422); }
function_exists guard keeps your handler closed if the plugin is deactivated.Send all the hfg_* fields with your other fields as an ordinary form-encoded or multipart POST to the same site, so the session cookie is sent. JSON request bodies are not read. Each attempt uses up its ticket, so for an AJAX form, reset the slider after every response, including validation errors:
window.HumanFormGuard?.reset(document.querySelector('#your-form-id'));
Your form still needs its own CSRF protection, permissions, validation and escaping. The older names hfg_render(), hfg_validate() and [human_form_guard] still work.
Appearance
Open Form Armour, Appearance. Settings are in five tabs beside a live preview: Layout, Slider, Colours, Visitor messages and Extras. Drag the preview to see each state, use Show error state, and switch the preview background between light and dark to check contrast. Saving keeps every tab's settings.
| Setting | Options | Plan |
|---|---|---|
| Alignment and width | Left, centre or right; a set width or the full form width | Free |
| When to show it | In the form, or in a pop-up when the visitor submits | Free |
| "Verify without dragging" button | Off by default. Runs the same checks | Free |
| Card style and handle shape | Soft card, outline or minimal; rounded with arrow, circle or square | Free |
| Size | Width 240 to 800 px, height 44 to 88 px, corner radius 0 to 44 px | Free |
| Accent, background and text colours | Any colour. The default accent is blue, #2563eb | Free |
| Visitor messages | Slider label, before verifying, checking, success, failure, expiry, button label, pop-up title and cancel, paste blocked | Free |
| Extras | Vibrate on success (on by default), "Protected by Form Armour" link (off by default) | Free |
| Theme presets | Emerald, Midnight, Slate, Sunset, Mono | Pro |
| Handle icon | Match the shape, double chevron, shield, padlock or none | Pro |
| Success, error and drag-trail colours | Any colour | Pro |
| Per-form styling | Choose a form under Editing to override the global settings | Pro |
Handle icons are white on dark accent colours and turn dark on light ones, such as lime, so they stay readable. The success tick follows the success colour the same way. Visitors who ask their device for reduced motion get the same states without animation.
Protection modes
Local
Needs no keys and makes no outside requests. A signed, single-use ticket tied to the form and the visitor's browser, a hidden field, a background puzzle, a minimum fill time, replay protection and a per-address limit. It stops scripts, replays and form-filling bots. It cannot stop a bot written specifically for this plugin.
Local + Turnstile (Pro)
Adds Cloudflare Turnstile's invisible bot check, with every token verified by your server against the hostname, action and ticket. Use it for forms that attract targeted abuse. To set it up:
- In Cloudflare, open Turnstile (a free account is enough), click Add widget, enter your site's domain and choose Managed.
- Copy the site key and secret key into Form Armour, Settings and save.
- Switch the forms you want to Local + Turnstile on the Forms screen.
To keep the secret out of the database, define it in wp-config.php instead:
define('HFG_TURNSTILE_SECRET', 'your-secret-key');
If Pro ends, Turnstile forms fall back to Local, so they stay protected. A key or Cloudflare failure never silently falls back to Local: the form fails closed until it is fixed.
How verification works
When a visitor first touches the form, their browser fetches a small single-use puzzle and solves it in the background. Dragging the slider trades the solved puzzle for a signed ticket. When the form is sent, your server runs these checks in order and stops at the first one that fails:
- Within the submission limit for this address.
- The hidden field is empty.
- The ticket is signed by your site, for this form.
- Not sent too fast (at least 3 seconds after the visitor started), and the ticket is less than five minutes old.
- Sent from the browser that earned the ticket.
- The slider was completed in the browser.
- The ticket has never been used before.
Each decision is recorded with one of these reasons:
| Reason | Meaning |
|---|---|
verified | Passed every check. |
rate_limit | Too many submissions from this address in ten minutes. |
honeypot_or_missing | The hidden field was filled in or missing. |
invalid_ticket | No ticket, a forged one, or one for another form or mode. |
expired_ticket | The ticket was more than five minutes old. |
too_fast | Sent less than the minimum fill time after the visitor started. |
session_mismatch | The ticket was earned in a different browser. |
interaction_missing | The slider was not completed. |
replay | The ticket had already been used. |
token_missing | Turnstile mode, but no Turnstile token was sent. |
turnstile_rejected | Cloudflare rejected the Turnstile token (Pro). |
token_context_mismatch | The Turnstile token was for another site, action or ticket (Pro). |
verification_unavailable | Cloudflare could not be reached. Counted as an error, not a block (Pro). |
verification_configuration | Cloudflare reported a problem with the secret key or the request, not with the visitor. Counted as an error (Pro). |
configuration | Turnstile is selected without keys. Counted as an error. |
storage_failure | The database could not record the ticket. Counted as an error. |
Settings and stats
- Submissions allowed per address every 10 minutes: 5 to 200, default 20. Visitors over the limit are blocked until the window passes. Shared networks, like offices and schools, may need a higher number.
- Keep statistics for: 7 to 90 days, default 30. An hourly WordPress cron job removes old tickets, limits and statistics, so make sure cron runs reliably.
No form contents and no raw IP addresses are stored. Address limits use salted, time-windowed hashes. The visitor's browser gets one session cookie, hfg_session_<site-id>, which is HttpOnly, Secure and SameSite=Strict. Mention it in your own site's cookie notice.
With Pro, the Dashboard shows totals and the Stats tab has the daily chart, details and CSV export. On the free plan, statistics are still recorded and those screens show a labelled sample. Counts are server-side decisions, not confirmed bots or delivered messages, and dates use UTC.
For developers
| Filter | Default | Use |
|---|---|---|
hfg_always_enqueue | false | Load the scripts on every page, for forms injected later by AJAX or pop-ups. |
hfg_min_fill_seconds | 3 | Minimum seconds between the visitor starting and the submission. 0 to 30. |
hfg_work_bits | 15 | Puzzle difficulty in leading zero bits. 8 to 22. Each step doubles the work for visitors' browsers. |
add_filter('hfg_always_enqueue', '__return_true');
add_filter('hfg_min_fill_seconds', fn() => 5);
The browser script exposes two methods for custom AJAX forms:
HumanFormGuard.reset(form)after every completed request, so the visitor can verify again.HumanFormGuard.reportError(form)when your server reports a verification failure. It opens the pop-up again.
Keep the REST endpoint /wp-json/form-armour/v1/challenge uncached and reachable by same-origin POST. Page HTML can be cached, because tickets are fetched separately.
Troubleshooting
Start with the Health check on the Settings screen. It shows "All good" and "Ready to protect your forms", plus any notes that apply to your site, such as "Your site is on HTTP" or "Limits are per browser here".
Every submission fails
Check that the challenge endpoint is not cached and that the form posts to the same address as your site. Visitors also need first-party cookies and JavaScript turned on.
Everyone is rate-limited together
Behind a proxy on your host's network, Form Armour reads the visitor's address automatically. A CDN with a public address still looks like one visitor until the real address is restored. Restore it at the web server, for example with mod_remoteip or nginx real_ip. Forwarding headers are never trusted blindly.
The slider does not appear
Check the form is saved as Local or Local + Turnstile, then clear page and plugin caches. If the form loads in a pop-up or by AJAX, turn on hfg_always_enqueue (see For developers).
A visitor cannot drag
The slider works with a keyboard. You can also turn on the Verify without dragging button under Appearance, Layout. It runs the same checks.
Pasting is blocked
Block pasting makes visitors type instead of pasting. It slows copy-paste spam but can get in the way of password managers and assistive tools, so use it only where it helps.
"Checking..." takes a few seconds
If someone verifies within 3 seconds of starting on the form, the slider holds the checking state until the minimum fill time has passed, rather than letting the submission be refused.
Pro, trial and licences
Start a free 3-day trial of Pro from the Upgrade screen inside Form Armour. No card is needed. The trial starts once you confirm it from the email Freemius sends; until then the screen says "Trial requested. Check your email." with a link to send it again. The badge in the Form Armour header shows Free, Trial pending, the days left in your trial, or Pro. Pro costs US$24.99 for one site or US$49.99 for up to three sites, paid once.
Pro adds Turnstile mode, per-form styling, theme presets, handle icons, success, error and drag-trail colours, the stats dashboard with daily chart and CSV export, and priority email support.
When a trial ends or a licence is removed, the site returns to Free. Turnstile forms fall back to Local so they stay protected, saved Pro styling is kept but not applied, and statistics keep being recorded.
Licences, trials, payments and updates are handled by Freemius, which sells Pro on Hyperpixel Studio's behalf. Freemius runs in anonymous mode in this plugin: there is no usage-tracking opt-in. Refunds are covered on the Refunds page.
Changelog
0.6.8
- The Health check explains why HTTPS is still worth adding, for your forms and your site. Form Armour keeps working either way.
- Added a reminder to test on a staging site first, with a link to our Terms.
0.6.7
- Form Armour now works on plain HTTP as well as HTTPS, with nothing to change on your server or host.
- Visitor addresses are worked out automatically behind a proxy on your host's network.
- The Health check no longer asks you to contact your host. It shows what Form Armour found and confirms it's ready.
0.6.6
- The new Form Armour logo now appears across the plugin's screens.
- The Upgrade page fits better on narrow screens.
0.6.5
- The Health check now explains each problem in plain language, with what it means for your forms and what to ask your host.
- Your trial status shows in the header while a trial is pending or running.
- The slider's trail now runs a little ahead of the handle as you drag.
- Rounder alerts and a clearer “Start free trial” confirmation.
0.6.4
- A new Stats tab. With Pro, it has the daily chart, details and CSV export.
- Other plugins' notices now wait in a Notices tray, so Form Armour screens stay focused.
- Form rows line up in columns, so names, protection settings and checkboxes are easier to scan.
- “Block pasting” now explains what it does and when to use it.
0.6.3
- Appearance settings are now tabs beside the live preview, with Save always in view.
- The Forms screen compares the two protection modes in one place and groups your forms by form plugin.
0.6.2
- A fresh look for the Form Armour screens: blue buttons and tabs, rounder corners and calmer fields.
- New installs use a blue slider by default. Colours you have already saved are kept.
- The Upgrade screen now matches the rest of Form Armour.
0.6.1
- Handle icons turn dark on light accent colours, such as lime, so the arrow and spinner stay easy to see. The live preview follows as you edit colours.
0.6.0
- Stronger protection: each visitor's browser quietly solves a small puzzle before a ticket is issued, so bots that only post the form are stopped.
- Submissions sent within 3 seconds of starting the form are blocked as too fast. Real visitors just see “Checking...” for a moment longer.
0.5.0
- A getting-started checklist, Help tabs and setup guides.
- Drag feedback: a progress trail, snap-back, and clear checking, success and error states, with optional vibration.
- Pro: handle icons, success and error colours, drag-trail colour, theme presets and a daily activity chart.
- An optional “Protected by Form Armour” link, off by default.
Removing the plugin
Deactivating stops protection and the scheduled clean-up, and keeps your settings and statistics. To delete everything when you uninstall, add this to wp-config.php first, then remove it afterwards:
define('HFG_DELETE_DATA', true);
Cloudflare Turnstile keys are not revoked automatically. Remove the widget in Cloudflare if you no longer need it.
