Skip to content
Antisy

Protect a WordPress form from spam bots, without a plugin

There is no Antisy plugin for WordPress: to keep spam bots off a contact form, the integration is done by hand, in a plugin of your own or in the theme. A tag in the <head>, then the PHP function of the documentation on the hook that receives the form: the response is allow or deny.

As of .

Is there a WordPress plugin?

No. Antisy offers no WordPress plugin to date, and this guide announces none. It shows what the product delivers today, a tag and a server call, which you plug into WordPress yourself.

The verification function is the one of the documentation. The extracts specific to WordPress (wp_head, pre_comment_on_post, admin_post_*) use standard hooks; they had not been run in a WordPress installation on the date of this page.

What you need

  • A site declared in the Antisy dashboard (in French), with the host names of your pages.
  • The site key (32 characters): it is public and goes in the page; the verification secret (64 characters) stays on your server and is shown only once, when it is created.
  • Pages served over HTTPS.
  • Access to a plugin of your own or to the theme (a child theme avoids losing the changes on an update), and to the wp-config.php file.
  • PHP 8.0 or later with the cURL extension at your host: the function of the documentation uses curl_init and the mixed type.

See: Documentation: keys and test environments

Is it available today?

Access is by waiting list. The API and the dashboard (in French) have been in service since 27 October 2026, hosted at Infomaniak in Geneva; no customer is in production yet.

The examples of this guide follow version 1 of the API, as the documentation describes it. To try them without protecting a real form, the documentation describes test keys; a test key never goes into production.

See: Documentation: test keys · The pilot · Join the waiting list

1. The two values

In wp-config.php, outside the public root of the repository of the theme. The function of the documentation reads the secret with getenv('ANTISY_SECRET'). At a host that offers no environment variable, a single line changes:

wp-config.php
<?php
// wp-config.php, above the line “That's all, stop editing!”.
define('ANTISY_SITE_KEY', 'VOTRE_CLE_DE_SITE');                  // public: it goes in the page
define('ANTISY_SECRET', 'VOTRE_SECRET_DE_VERIFICATION');         // secret: never in the repository of a theme

One line of the function
// In antisy.php, replace this line of the function of the documentation:
'secret' => getenv('ANTISY_SECRET'),
// with this one:
'secret' => ANTISY_SECRET,

2. The tag in every page

The tag is printed by the wp_head hook, with the site key:

Plugin or functions.php
<?php
// In a plugin of your own, or in the functions.php of the theme: the tag, in the <head> of every page.
add_action('wp_head', function () {
    if (!defined('ANTISY_SITE_KEY')) {
        return;
    }
    printf(
        '<script src="https://api.antisy.com/v1/i/l.js" data-site="%s" defer></script>' . "\n",
        esc_attr(ANTISY_SITE_KEY)
    );
});

// The antisy_verdict() function of the documentation, saved in antisy.php next to this file.
require_once __DIR__ . '/antisy.php';

The token is not in the HTML of the page: the script creates it on submission, in the browser. A page served by a page cache or by a CDN therefore works like any other. If an optimisation plugin defers, combines or rewrites the scripts, check that the tag arrives intact: not verified with the common plugins.

3. The verification function

Save the PHP function of the documentation in antisy.php, next to the previous file, without changing it apart from the line of the secret:

antisy.php
<?php
/**
 * Vérifie le jeton Antisy (mode simple). Rend le verdict : "allow" ou "deny".
 *
 * - jeton vide ou absent : envoyé tel quel, Antisy décide ; jeton non vide inutilisable : "deny", sans appel ;
 * - Antisy ne répond pas (réseau, délai, 5xx) : "allow" et on journalise (fail-open) ;
 * - 429 : ANTISY_ON_RATE_LIMIT ;
 * - tout autre statut que 200 (400, 401, 413, 415...) : erreur de VOTRE appel, journalisée,
 *   tentative non vérifiée : "deny". Jamais "allow".
 */
const ANTISY_ON_RATE_LIMIT = 'allow'; // 'deny' for a sensitive form: an attacker can cause a 429

function antisy_verdict(mixed $token): string
{
    if ($token === null) {
        $token = ''; // missing field: treated as empty
    }
    // Empty: sent as it is, Antisy decides (collection on: deny; off: the engine decides).
    // Not empty: 1 to 2,048 printable ASCII characters; /D: “$” does not tolerate a final line break.
    if ($token !== '' && (!is_string($token) || !preg_match('/^[\x20-\x7e]{1,2048}$/D', $token))) {
        return 'deny'; // a form array, a token too long or crafted: what Antisy would answer
    }

    $ch = curl_init('https://api.antisy.com/v1/siteverify');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 3, // seconds; beyond that, fail-open
        CURLOPT_FOLLOWLOCATION => false, // no redirect followed (the default, written so that no one turns it on): a 3xx is an error of YOUR call
        CURLOPT_POSTFIELDS => http_build_query([
            'secret' => getenv('ANTISY_SECRET'), // siteverify secret; never hard-coded nor on the client side
            'response' => $token,
        ]),
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $error = curl_error($ch);
    curl_close($ch);

    // The only case where the submission is let through: Antisy does not answer.
    if ($body === false || $status >= 500) {
        error_log("Antisy injoignable, envoi laissé passer (fail-open): {$error} (HTTP {$status})");
        return 'allow';
    }
    if ($status === 429) {
        error_log('Antisy 429 (voie saturée): ' . ANTISY_ON_RATE_LIMIT);
        return ANTISY_ON_RATE_LIMIT;
    }
    if ($status !== 200) {
        error_log("Antisy HTTP {$status} : erreur de l'appel, à corriger ; tentative non vérifiée");
        return 'deny';
    }
    $result = json_decode($body, true);
    if (is_array($result) && is_bool($result['success'] ?? null)) {
        if ($result['success']) {
            return 'allow'; // the only acceptance
        }
        return 'deny'; // success false: refusal (the verdict is “deny”)
    }
    error_log('Réponse Antisy illisible, tentative non vérifiée');
    return 'deny';
}

4. Protect comments

The pre_comment_on_post hook runs before WordPress processes a comment:

Plugin or functions.php
<?php
// Comments: the WordPress form sends its fields to wp-comments-post.php, on your site.
// This hook runs before the comment is processed.
add_action('pre_comment_on_post', function () {
    if (antisy_verdict($_POST['antisy-token'] ?? '') !== 'allow') {
        wp_die('Envoi refusé.', 'Envoi refusé', ['response' => 403, 'back_link' => true]);
    }
});

WordPress sends the comment form to wp-comments-post.php, on your site: it is a POST to your own origin, which the script covers. wp_die answers 403 and offers a link back.

5. Protect a hand-written WordPress contact form

The form sends to admin-post.php, which is on your site, with a hidden action field that chooses the hook:

Template of the theme
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
  <input type="hidden" name="action" value="antisy_contact">
  <label>Nom <input name="name" autocomplete="name"></label>
  <label>E-mail <input name="email" type="email" autocomplete="email"></label>
  <label>Message <textarea name="message"></textarea></label>
  <button>Envoyer</button>
</form>

Plugin or functions.php
<?php
function antisy_contact_handler(): void
{
    if (antisy_verdict($_POST['antisy-token'] ?? '') !== 'allow') {
        wp_die('Envoi refusé.', 'Envoi refusé', ['response' => 403, 'back_link' => true]);
    }

    // … clean the fields, then send or save the message (wp_mail, post, database…)

    wp_safe_redirect(home_url('/merci/'));
    exit;
}
add_action('admin_post_nopriv_antisy_contact', 'antisy_contact_handler'); // visitor not logged in
add_action('admin_post_antisy_contact', 'antisy_contact_handler');        // visitor logged in

This example deals only with the bot check. Cleaning the fields, sending the message and the protections specific to WordPress remain yours.

What about form plugins?

Form plugins (contact, quote, registration) each have their own way of receiving and validating a submission. This guide does not cover them, nor the WordPress login page: not verified. The script covers a POST form sent to your site even when the page sends it by fetch, but the server-side verification must be plugged in where the plugin validates the submission, which this guide does not describe.

See: Documentation: which forms the script covers

What does the verification answer?

The response is a JSON object. success is true if and only if verdict is allow. There are only these two outcomes, and the response never gives the reason: the refusal you show the visitor is generic too.

A token is valid for 60 seconds and can be verified only once. A second call with the same token is refused.

Response of POST /v1/siteverify (HTTP 200)
{
  "success": true,
  "verdict": "allow",
  "flags": [],
  "error-codes": [],
  "challenge_ts": "2026-09-24T10:00:00Z",
  "hostname": "shop.example",
  "action": "contact",
  "event_id": "evt_01J7X4M2Q9K8ZP3R5T6V7W8Y9A"
}

See: Documentation: the request, the response and the error codes

What if Antisy does not answer?

The function lets the submission through in one case only: Antisy does not answer (network down, deadline of 3 seconds exceeded, 5xx error). An Antisy outage must not close your form.

Any other HTTP error (400, 401, 413, 415) comes from your call: the function refuses the submission and logs it, never a pass. A 429 is a decision that is yours: the constant ANTISY_ON_RATE_LIMIT is allow by default; set deny for a sensitive form.

See: Documentation: the request, the response and the error codes

What happens if the script is blocked?

A CSP policy that refuses the script, an ad blocker, a page served over HTTP or a visitor without JavaScript stop the script from running. The form is then sent anyway, without the antisy-token field: read it with an empty default value.

What follows depends on the site’s “Collecte du navigateur” (browser collection) setting, in the dashboard, which is in French. Collection on, a submission without a token does not get through; collection off, a missing token is not held against it. A new site starts with collection off.

See: Documentation: if the script does not run

What a bot check does not replace

No bot check is perfect, and Antisy announces no detection rate. It adds to the other defences of a form, such as limiting the number of submissions; it does not replace them.

Frequently asked questions

Is there an Antisy plugin for WordPress?

No. Antisy offers no WordPress plugin to date. The integration is done by hand: a tag in the <head> of the theme or of a plugin of your own, and the PHP function of the documentation on the hook that receives the form.

Where should the Antisy secret go in WordPress?

In an environment variable of the server if your host offers one, otherwise in wp-config.php, with the site key. The secret never goes in the repository of a theme, in the page or in a log.

Does a page cache or a CDN get in the way of the Antisy token?

The token is not in the HTML: the script creates it on submission, in the browser of each visitor. A cached page therefore works like any other. The risk lies elsewhere: an optimisation plugin that rewrites the scripts can damage the tag, and this guide has not checked that with the common plugins.

See also: Protect a PHP form from spam bots, without a puzzle · Protect a Next.js form from bots, without a puzzle · All guides · Documentation: step-by-step guide