Aller au contenu
Antisy
Documentation

Vérifier un jeton

Votre serveur reçoit le formulaire avec le champ antisy-token, que le script a rempli, et le fait vérifier par un seul appel. La réponse dit si le visiteur est accepté.

La requête

POST https://api.antisy.com/v1/siteverify, avec un corps application/x-www-form-urlencoded ou application/json.

secret
Obligatoire. Votre secret de vérification (64 caractères alphanumériques), dans le corps et nulle part ailleurs.
response

Le jeton, tel qu’il est arrivé dans le champ antisy-token (rempli par le script) ou l’en-tête x-antisy-token (posé par votre page). Une chaîne de 1 à 2048 caractères ASCII imprimables. Vide ou absent, il est lu comme un jeton absent.

Contrôlez-le chez vous avant l’appel. Hors de cette forme, l’API le traite comme un jeton forgé : 200, success à false, verdict à deny, invalid-input-response.

idempotency_key
Facultatif. Un UUID ; toute autre valeur est refusée. Voir plus bas.
remoteip
Ne l’envoyez pas. Il est ignoré, quelle que soit sa valeur, et jamais refusé : retirez-le.
  • N’envoyez pas d’en-tête Authorization. Sa présence bascule la requête vers un autre mode, celui du SDK, où le secret n’est pas un justificatif valable : réponse 401, dans un corps d’une autre forme. Un secret envoyé en Authorization: Bearer est refusé de la même façon.
  • Appelez la route depuis votre serveur, jamais depuis un navigateur : elle n’envoie aucun en-tête CORS, un navigateur ne peut pas lire la réponse.

La réponse

Réponse, statut 200
{
  "success": true,
  "verdict": "allow",
  "flags": [],
  "error-codes": [],
  "challenge_ts": "2026-09-24T10:00:00Z",
  "hostname": "shop.example",
  "action": "contact",
  "event_id": "evt_01J7X4M2Q9K8ZP3R5T6V7W8Y9A"
}

success
true si et seulement si le visiteur est accepté. C’est le champ à lire si vous ne voulez qu’un booléen.
verdict
allow ou deny, et rien d’autre. success vaut true si et seulement si verdict vaut allow : les deux disent la même chose.
flags
La liste des états qui ne sont pas des erreurs. Elle est ouverte : ignorez ceux que vous ne connaissez pas (voir plus bas).
error-codes
La liste des codes d’erreur, vide quand rien ne cloche (voir plus bas).
challenge_ts
L’heure du dépôt du jeton, au format ISO 8601 en UTC, sur un allow qui a un dépôt derrière lui. Un allow sans dépôt le porte à null, par exemple : collecte éteinte et jeton absent, site ou organisation suspendus, jeton de test, dépôt que notre base n’a pas pu écrire (avec internal-error). Ne le rendez donc pas obligatoire pour accepter un visiteur. null aussi sur tout refus, quelle qu’en soit la cause.
hostname
Sur un allow, le nom d’hôte de la page qui a demandé le jeton, quand son origine est attestée. null sinon, et sur tout refus.
action
Sur un allow, l’étiquette passée à token() ou posée par data-antisy-action sur le formulaire, ou null s’il n’y en avait pas. null aussi sur tout refus.
event_id
L’identifiant de cette vérification, de la forme evt_ suivi de 26 caractères. Citez-le si vous écrivez au support (support@antisy.com) à propos d’un cas.

La réponse ne contient ni score, ni seuil, ni raison, dans aucun mode : un verdict, jamais le détail qui l’a produit. Chaque raison rendue apprendrait à celui qui contourne ce qui l’a arrêté, et les raisons restent chez Antisy. Les codes d’erreur disent ce que votre intégration doit corriger, pas ce que la détection a vu. Un leurre et un envoi refusé pour ce que sa fabrication prouve reçoivent la même réponse que n’importe quel refus. Elle porte Cache-Control: no-store et l’en-tête X-Antisy-Api-Version: 1.

Les indicateurs

test_key
La vérification a été faite avec un secret de test.
over_quota
Le quota mensuel du palier Gratuit est dépassé : le score ne décide plus la réponse, mais la porte reste (un jeton absent tant que la collecte du site est allumée est refusé, comme un jeton invalide, falsifié, rejoué, expiré, d’un autre site ou dont le calcul n’a pas fini à temps, et un visiteur de votre liste de blocage aussi). Un état de votre compte, à journaliser.
degraded
Une source d’Antisy n’était pas disponible pour cette vérification : la réponse est rendue sans elle. Journalisez-le (voir internal-error).
idempotent_replay
Réponse rejouée : même idempotency_key dans les 60 secondes.

Des champs, des indicateurs et des codes peuvent s’ajouter sans changer de version. Une rupture de compatibilité créerait une route /v2/ : lisez ce que vous connaissez et ignorez le reste.

Que faire de chaque verdict

allow

success à true. Publiez ou enregistrez le message.

deny

success à false. Le visiteur ne passe pas : refus générique, sans révéler la raison. Le visiteur n’apprend rien de ce qui s’est passé ; seul votre serveur le sait.

Il n’y a que ces deux issues. Si vous voulez une modération ou une confirmation par e-mail, c’est vous qui la décidez, sur vos propres signaux. À quel point Antisy refuse est un réglage de votre site, sur le tableau de bord : Strict, Équilibré (par défaut) ou Permissif, décrits sur le démarrage rapide.

Cet appel convient aux formulaires à faible enjeu. Il ne lie pas le jeton à un compte : ne l’utilisez pas pour une connexion ou une inscription. Le SDK Node (Express, Fastify, Next.js) sera publié sur npm avec sa documentation ; il n’est pas encore publié.

Ce qui ne passe jamais

Quel que soit le mode de vérification de votre site, ces cas sont refusés : success à false, verdict à deny.

  • Un jeton absent, sur un site dont la collecte du navigateur est allumée (missing-input-response). Sur un site dont la collecte est éteinte, l’absence n’est pas reprochée : un site neuf naît avec la collecte éteinte.
  • Un jeton inventé, illisible, d’une forme hors des limites, ou émis pour un autre de vos sites (invalid-input-response).
  • Un jeton déjà vérifié, ou expiré, même de quelques secondes (timeout-or-duplicate).
  • Un envoi dont la fabrication est prouvée : un programme l’a fabriqué plutôt que le navigateur. La réponse ne porte aucun code d’erreur et ne dit pas ce qui l’a trahi.
  • Un jeton dont le calcul en arrière-plan n’a pas fini à temps (appareil très lent, onglet caché, délai totalTimeoutMs trop court) : il part sans sa solution. Ce n’est pas une preuve de fabrication, mais il est refusé de même, sans code d’erreur ; un nouvel essai refait le calcul avec un jeton neuf (le délai du calcul).
  • Un jeton absent ou invalide n’est pas rattrapé par une liste d’autorisation, quand Antisy en a posé une pour votre site, à votre demande écrite au support : elle dispense de l’évaluation, pas du jeton.

Cette règle tient aussi au-delà du quota mensuel du palier Gratuit. Le score ne décide alors plus la réponse : un envoi qui n’a rien de ceux-là passe, avec l’indicateur over_quota. Mais la porte reste fermée : un jeton absent (collecte allumée), invalide, falsifié, rejoué, expiré ou d’un autre site, ou dont le calcul n’a pas fini à temps, reçoit success à false comme sous le quota, avec les mêmes codes d’erreur et over_quota dans flags. L’indicateur ne change donc rien à votre code : c’est un état de votre compte, à journaliser. Vos envois sans jeton ou avec un faux jeton comptent, comme avant, dans le quota. Une liste de blocage posée par Antisy pour votre site, à votre demande écrite au support (le tableau de bord ne gère pas ces listes), tient aussi : un visiteur bloqué reçoit success à false hors quota, avec ou sans jeton ; une liste d’autorisation, elle, ne dispense pas du jeton.

Durée de validité et usage unique

  • Un jeton est valable 60 secondes après son dépôt. Passé ce délai, la vérification rend success à false et timeout-or-duplicate, quel que soit le reste.
  • Il ne se vérifie qu’une fois. Un second appel avec le même jeton rend timeout-or-duplicate.

Rejouer un appel : idempotency_key

Si votre appel a peut-être abouti mais que vous n’avez pas lu la réponse (coupure, délai), le renvoyer sans précaution donnerait timeout-or-duplicate. Envoyez une idempotency_key (un UUID) : un nouvel appel avec la même clé dans les 60 secondes rejoue le même verdict et le même event_id, avec l’indicateur idempotent_replay, sans revérifier le jeton. Les codes d’erreur de la première réponse ne sont pas repris : error-codes est vide, et flags ne porte que idempotent_replay, plus over_quota si la première réponse le portait. Ce rejeu suppose que la première réponse a été gardée : ce n’est jamais le cas avec un secret de test, ni quand le premier appel a échoué avant de la garder. Le jeton est alors réévalué sans être recompté : un nouvel event_id, sans idempotent_replay, et un verdict qui peut différer du premier.

Votre serveur génère la clé : un UUID neuf par soumission, repris seulement pour réessayer cet appel. Ne la lisez jamais dans un champ du formulaire, un en-tête ou la requête du visiteur : une clé que le visiteur choisit lui permettrait de rejouer la réponse d’une soumission déjà acceptée, et de passer encore avec le même jeton pendant 60 secondes.

curl, avec une clé d’idempotence
# Une clé par jeton : créée une fois, puis reprise telle quelle à chaque nouvelle tentative.
KEY=$(uuidgen)
for tentative in 1 2 3; do
  printf 'secret=%s' "$ANTISY_SECRET" | curl -sS --max-time 3 https://api.antisy.com/v1/siteverify \
    -d @- \
    --data-urlencode response="$ANTISY_TOKEN" \
    -d idempotency_key="$KEY" && break
  sleep 1
done

Codes d’erreur

Les cinq premiers codes accompagnent une réponse 200 qui porte un verdict. Un secret absent ou refusé n’est donc pas une erreur HTTP : c’est un refus (deny), pour que la route ne serve pas à tester des secrets et qu’une erreur de configuration se voie dès le premier essai, sans laisser votre site ouvert. Les trois derniers sont des statuts d’erreur, dans un corps sans verdict :

Corps d’une erreur (429)
{ "success": false, "error-codes": ["rate-limited"] }

missing-input-secretStatut 200

Le champ secret manque dans le corps de la requête. Ce n’est pas une erreur HTTP : la réponse est un refus (success à false, verdict deny), pour qu’une erreur de configuration se voie dès le premier essai.

Corrigez l’appel. Tant qu’il est faux, chaque visiteur est refusé : journalisez-le comme une erreur.

invalid-input-secretStatut 200

Le secret est inconnu, révoqué, mal formé ou d’un autre type (une clé d’évaluation n’est pas un secret de vérification), ou le site est fermé. Même réponse que pour un secret absent : la route ne dit jamais rien de plus.

Vérifiez la clé dans l’onglet Clés du site. Même effet que le précédent : chaque visiteur est refusé.

missing-input-responseStatut 200

Le champ response est vide, sur un site dont la collecte du navigateur est allumée. Le visiteur est refusé, quel que soit le mode de vérification du site.

Le script n’a pas tourné, ou le formulaire n’est pas couvert par le script et la page n’a pas appelé token(). Voir la page sur le script navigateur.

invalid-input-responseStatut 200

Le jeton n’est pas valable pour ce site : inventé, altéré, émis pour un autre de vos sites, ou d’une forme hors des limites (plus de 2 048 caractères, caractères non imprimables). Le visiteur est refusé.

Vérifiez que vous transmettez le jeton tel qu’il est arrivé, sans le tronquer, et qu’il vient bien de la page de ce site.

timeout-or-duplicateStatut 200

Le jeton a déjà été vérifié, ou il a plus de 60 secondes. Le visiteur est refusé.

Ne vérifiez un jeton qu’une fois. Pour rejouer un appel après une coupure, envoyez idempotency_key. Sinon, faites renvoyer le formulaire : le navigateur produira un nouveau jeton.

bad-requestStatut 400, 405, 415

Requête mal formée par votre appel : corps illisible, type de contenu ou jeu de caractères non accepté, corps compressé, champ inconnu, idempotency_key qui n’est pas un UUID, méthode autre que POST. Le corps ne porte pas de verdict.

Le défaut est dans votre appel. Journalisez-le comme un bug. L’envoi n’a pas été vérifié : ne le laissez pas passer, refusez-le.

rate-limitedStatut 429

Trop d’appels sur cette voie. L’en-tête Retry-After donne le délai, en secondes. Un attaquant peut provoquer ce statut par le volume.

Par défaut, laissez passer et journalisez. Pour un formulaire sensible, refusez : voir « Quand l’API ne répond pas ».

internal-errorStatut 500, 503

Panne chez Antisy, base de données indisponible par exemple. Le code peut aussi accompagner un allow : chez Antisy, ce jeton n’a pas pu être enregistré.

Laissez passer l’envoi et journalisez-le.

Une autre méthode que POST reçoit 405, dans le corps d’erreur montré plus haut, avec bad-request.

Un site ou une organisation suspendus : votre secret répond alors success à true et verdict à allow, sans indicateur ni code d’erreur, hormis test_key pour un secret de test. Une suspension de compte ne casse pas vos formulaires, mais rien n’est vérifié pendant ce temps. Un site fermé, lui, est refusé comme un secret inconnu (invalid-input-secret).

Quand l’API ne répond pas

Règle : laissez passer seulement quand Antisy ne répond pas

Si vous écrivez « si success est faux, refuser », vous bloquerez tous vos formulaires pendant une panne d’Antisy. Une panne chez nous ne doit pas coûter vos visiteurs. Mais « laisser passer » ne vaut que pour une vraie absence de réponse : un robot qui provoque une erreur ne doit pas en tirer un laissez-passer.

  1. Contrôlez le jeton avant l’appel. Un jeton présent est une chaîne de 1 à 2048 caractères ASCII imprimables. Sinon, n’appelez pas Antisy : l’envoi n’est pas vérifié, refusez-le. Un jeton vide, lui, part tel quel : Antisy sait si le site attend un jeton.
  2. Laissez passer, et journalisez, seulement sur une erreur réseau, un échec DNS, un délai dépassé (3 secondes est le délai que nous recommandons) ou une réponse 5xx.
  3. Ne laissez jamais passer sur 400, 401, 413, 415 ni sur aucun autre statut 4xx à part le 429. C’est un défaut de votre appel : journalisez-le comme un bug à corriger, et traitez l’envoi comme non vérifié : refusez-le.
  4. Le 429 est un choix. Retry-After donne le délai en secondes. Avec un secret de test, un 429 peut aussi dire que vos secrets de test ont épuisé leurs 1 000 appels du jour, ou ceux du mois (limites) : il dure alors jusqu’au prochain minuit UTC, ou jusqu’au premier du mois UTC suivant. Nos exemples laissent passer par défaut, comme pour une panne. Mais un attaquant peut provoquer un 429 par le volume : la voie est limitée par clé, et un flot de soumissions sur votre formulaire atteint cette limite, pour tous vos visiteurs, tant qu’il dure ; ses envois passent aussi. Pour un formulaire sensible, refusez sur un 429 (deny) : c’est le choix protecteur, au prix de refuser des visiteurs légitimes pendant une saturation, qui réessaieront après Retry-After. Le comportement par défaut est réglé par une constante dans l’exemple Node. Un 503 qui porte Retry-After est un délestage : l’instance a atteint sa capacité d’accueil, et un tiers peut le provoquer par le volume, comme le 429. Les exemples le traitent comme une panne ; pour un formulaire sensible, refusez aussi sur ce 503, avec la même valeur que pour le 429.
  5. Un secret absent ou refusé ne relève pas de cette règle : la réponse est un refus à 200 (deny). Journalisez missing-input-secret et invalid-input-secret comme des erreurs de configuration : tous vos visiteurs sont refusés tant qu’elles durent, et rien d’autre ne casse.

Laisser passer pendant une panne est un choix : des robots peuvent passer avec. C’est le prix de ne jamais bloquer un visiteur à cause d’Antisy.

Exemples par langage : PHP, Python, Go

La vérification est un appel HTTP : aucune bibliothèque à installer. Voici le même exemple en PHP, en Python et en Go, avec les mêmes règles que les exemples curl et Node du démarrage rapide.

Ce que chaque exemple fait

  1. Il contrôle le jeton avant l’appel. Un jeton présent est une chaîne de 1 à 2048 caractères ASCII imprimables ; sinon l’exemple ne contacte pas Antisy et refuse, comme Antisy le ferait. Un jeton vide part tel quel : Antisy sait si le site attend un jeton.
  2. Il appelle POST /v1/siteverify, avec un corps de formulaire qui porte secret et response, et un délai de 3 secondes. Le secret vient de la variable d’environnement ANTISY_SECRET : il n’est jamais en dur, jamais en argument de ligne de commande, jamais dans un en-tête.
  3. Il ne lit que success : true est la seule acceptation. Une réponse illisible est un envoi non vérifié, donc refusé.
  4. Il laisse passer seulement quand Antisy ne répond pas : erreur réseau, délai dépassé, réponse 5xx. Il journalise chaque fois.
  5. Il ne laisse jamais passer sur une erreur de votre appel : 400, 401, 413, 415 et tout autre 4xx, ni sur une redirection 3xx, qu’il ne suit pas : une redirection rejouerait le corps, secret compris, vers une autre adresse.

Le 429 est un choix qui vous appartient : une constante en tête de chaque exemple le règle, allow par défaut. Pour un formulaire sensible, mettez deny ; la page quand l’API ne répond pas dit pourquoi. Les exemples traitent tout 5xx comme une panne ; pour refuser aussi sur un 503 qui porte Retry-After, traitez-le avant le cas des 5xx, avec la même valeur.

Les exemples sont ceux du guide de l’API. Les tests d’Antisy les lancent contre un faux serveur local, sur les mêmes cas : réponse acceptée, refusée, panne, 429, erreur de votre appel, réponse illisible, délai dépassé et redirections.

PHP

Avec l’extension curl de PHP (curl_init) et PHP 8 ou plus : la signature de la fonction utilise le type mixed.

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' pour un formulaire sensible : un attaquant peut provoquer un 429

function antisy_verdict(mixed $token): string
{
    if ($token === null) {
        $token = ''; // champ absent : traité comme vide
    }
    // Vide : envoyé tel quel, Antisy décide (collecte allumée : deny ; éteinte : le moteur décide).
    // Non vide : 1 à 2 048 caractères ASCII imprimables ; /D : « $ » ne tolère pas un saut de ligne final.
    if ($token !== '' && (!is_string($token) || !preg_match('/^[\x20-\x7e]{1,2048}$/D', $token))) {
        return 'deny'; // un tableau de formulaire, un jeton trop long ou piégé : ce que répondrait Antisy
    }

    $ch = curl_init('https://api.antisy.com/v1/siteverify');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 3, // secondes ; au-delà, fail-open
        CURLOPT_FOLLOWLOCATION => false, // aucune redirection suivie (c'est le défaut, écrit pour qu'on ne l'allume pas) : une 3xx est une erreur de VOTRE appel
        CURLOPT_POSTFIELDS => http_build_query([
            'secret' => getenv('ANTISY_SECRET'), // secret siteverify ; jamais en dur ni côté client
            'response' => $token,
        ]),
    ]);
    $body = curl_exec($ch);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $error = curl_error($ch);
    curl_close($ch);

    // Seul cas où l'on laisse passer : Antisy ne répond pas.
    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'; // la seule acceptation
        }
        return 'deny'; // success false : refus (le verdict vaut « deny »)
    }
    error_log('Réponse Antisy illisible, tentative non vérifiée');
    return 'deny';
}

Dans votre gestionnaire de formulaire
// Dans le gestionnaire de votre formulaire de contact :
$verdict = antisy_verdict($_POST['antisy-token'] ?? '');
if ($verdict === 'allow') {
    // publier / enregistrer le message
} else {
    // deny : refus générique, sans dire pourquoi
}

Python

Bibliothèque standard seulement. urllib suit par défaut une redirection 301, 302 ou 303 : l’exemple installe un ouvreur qui n’en suit aucune.

antisy.py
import http.client
import json
import logging
import os
import re
import urllib.error
import urllib.parse
import urllib.request

API = "https://api.antisy.com/v1/siteverify"
# 429 : "allow" (défaut, comme le kit Node) ; "deny" pour un formulaire sensible, car un
# attaquant peut provoquer un 429 par le volume.
ON_RATE_LIMIT = "allow"
# 1 à 2 048 caractères ASCII imprimables ; `fullmatch` (et non `$`, qui tolère un saut de ligne final).
USABLE_TOKEN = re.compile(r"[\x20-\x7e]{1,2048}")


class _NoRedirect(urllib.request.HTTPRedirectHandler):
    """Aucun suivi de redirection : urllib suit un 301, 302 ou 303 par défaut ; la réponse 3xx est alors une erreur de VOTRE appel."""

    def redirect_request(self, *args, **kwargs):
        return None


_OPENER = urllib.request.build_opener(_NoRedirect)


def antisy_verdict(token) -> str:
    """Rend le verdict Antisy : "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 : 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".
    """
    if token is None:
        token = ""  # champ absent : traité comme vide
    # Vide : envoyé tel quel, Antisy décide (collecte allumée : deny ; éteinte : le moteur décide).
    if token != "" and (not isinstance(token, str) or USABLE_TOKEN.fullmatch(token) is None):
        return "deny"  # trop long, piégé, ou pas une chaîne : ce que répondrait Antisy

    data = urllib.parse.urlencode(
        {
            "secret": os.environ["ANTISY_SECRET"],  # secret siteverify ; jamais côté client
            "response": token,
        }
    ).encode()
    try:
        with _OPENER.open(urllib.request.Request(API, data=data), timeout=3) as response:
            body = response.read()
    except urllib.error.HTTPError as error:
        if error.code >= 500:
            logging.warning("Antisy HTTP %s, envoi laissé passer (fail-open)", error.code)
            return "allow"
        if error.code == 429:
            logging.warning("Antisy 429 (voie saturée) : %s", ON_RATE_LIMIT)
            return ON_RATE_LIMIT
        logging.error("Antisy HTTP %s : erreur de l'appel, à corriger ; tentative non vérifiée", error.code)
        return "deny"
    except (urllib.error.URLError, http.client.HTTPException, OSError) as error:
        # Réseau, délai, DNS, RemoteDisconnected, ConnectionResetError, IncompleteRead : Antisy n'a
        # pas répondu (URLError, TimeoutError et ConnectionError sont des OSError ; on les nomme pour
        # qui lit). C'est le seul cas, avec le 5xx, où l'on laisse passer.
        logging.warning("Antisy injoignable (%s), envoi laissé passer (fail-open)", error)
        return "allow"
    try:
        result = json.loads(body)
        success = result.get("success")
    except (ValueError, AttributeError):
        success = None
    if success is True:
        return "allow"  # la seule acceptation
    if success is False:
        return "deny"
    logging.error("Réponse Antisy illisible, tentative non vérifiée")
    return "deny"

Dans votre gestionnaire de formulaire
# Dans le gestionnaire de votre formulaire de contact :
verdict = antisy_verdict(form.get("antisy-token", ""))
if verdict == "allow":
    pass  # publier / enregistrer le message
else:
    pass  # deny : refus générique, sans dire pourquoi

Go

Bibliothèque standard seulement. Le client HTTP est le vôtre, avec un délai de 3 secondes : celui de http.DefaultClient est infini. Le contrôle du jeton est une boucle, parce que les expressions régulières de Go refusent un compteur de répétition au-dessus de 1 000.

antisy/verify.go
// Package antisy vérifie un jeton Antisy en mode simple : Verdict rend "allow" ou "deny". Mêmes règles que les exemples ci-dessus.
package antisy

import (
	"encoding/json"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"time"
)

const apiURL = "https://api.antisy.com/v1/siteverify"

// 429 : "allow" (défaut, comme le kit Node) ; "deny" pour un formulaire sensible, car un
// attaquant peut provoquer un 429 par le volume.
const onRateLimit = "allow"

// 1 à 2 048 caractères ASCII imprimables (0x20 à 0x7E). Une boucle et non une expression régulière : celles de Go refusent un
// compteur de répétition au-dessus de 1 000 (`{1,2048}` ne compile pas), et un saut de ligne final n'y passe pas non plus.
func usableToken(token string) bool {
	if len(token) < 1 || len(token) > 2048 {
		return false
	}
	for i := 0; i < len(token); i++ {
		if token[i] < 0x20 || token[i] > 0x7e {
			return false
		}
	}
	return true
}

// Un client à lui, avec un délai de 3 s (celui de http.DefaultClient est infini) ; au-delà, fail-open.
// Aucun suivi de redirection : un 307 rejouerait le corps, secret compris, vers une autre adresse.
var client = &http.Client{
	Timeout:       3 * time.Second,
	CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
}

// Verdict rend le verdict Antisy : "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, réponse coupée, 5xx) : "allow" et on journalise (fail-open) ;
//   - 429 : onRateLimit ;
//   - tout autre statut que 200 (400, 401, 413, 415...) : erreur de VOTRE appel, journalisée,
//     tentative non vérifiée : "deny". Jamais "allow".
func Verdict(token string) string {
	// Vide : envoyé tel quel, Antisy décide (collecte allumée : deny ; éteinte : le moteur décide).
	if token != "" && !usableToken(token) {
		return "deny" // trop long ou piégé : ce que répondrait Antisy
	}

	response, err := client.PostForm(apiURL, url.Values{
		"secret":   {os.Getenv("ANTISY_SECRET")}, // secret siteverify ; jamais en dur ni côté client
		"response": {token},
	})
	if err != nil {
		// Réseau, DNS, délai dépassé : Antisy n'a pas répondu. Seul cas, avec le 5xx, où l'on laisse passer.
		// err peut citer l'URL de l'appel, jamais le corps : le secret n'est pas dans le journal.
		log.Printf("antisy injoignable, envoi laissé passer (fail-open): %v", err)
		return "allow"
	}
	defer response.Body.Close()
	body, readErr := io.ReadAll(io.LimitReader(response.Body, 1<<20))

	switch {
	case response.StatusCode >= 500:
		log.Printf("antisy HTTP %d, envoi laissé passer (fail-open)", response.StatusCode)
		return "allow"
	case response.StatusCode == http.StatusTooManyRequests:
		log.Printf("antisy 429 (voie saturée) : %s", onRateLimit)
		return onRateLimit
	case response.StatusCode != http.StatusOK:
		log.Printf("antisy HTTP %d : erreur de l'appel, à corriger ; tentative non vérifiée", response.StatusCode)
		return "deny" // 400, 401, 413, 415... jamais « allow »
	}
	if readErr != nil {
		log.Printf("antisy : réponse coupée (%v), envoi laissé passer (fail-open)", readErr)
		return "allow" // Antisy n'a pas fini de répondre : c'est une panne, pas un refus
	}
	var result struct {
		Success *bool `json:"success"`
	}
	if json.Unmarshal(body, &result) != nil || result.Success == nil {
		log.Printf("antisy : réponse illisible, tentative non vérifiée")
		return "deny"
	}
	if *result.Success {
		return "allow" // la seule acceptation
	}
	return "deny"
}

Dans votre gestionnaire de formulaire
// Dans le gestionnaire de votre formulaire de contact (net/http) :
func contact(w http.ResponseWriter, r *http.Request) {
	// PostFormValue rend "" quand le champ est absent : traité comme vide.
	if antisy.Verdict(r.PostFormValue("antisy-token")) != "allow" {
		http.Error(w, "refusé", http.StatusForbidden) // deny : refus générique, sans dire pourquoi
		return
	}
	// publier / enregistrer le message
}

Un autre langage

Recopiez ce que font les exemples, pas leur syntaxe.

  • Le corps est un formulaire (application/x-www-form-urlencoded) qui porte secret et response, ou un objet JSON des mêmes champs.
  • Pas d’en-tête Authorization : sa présence bascule la requête vers un autre mode (la requête).
  • Aucun suivi de redirection, et un délai de 3 secondes.
  • Les mêmes cas, dans le même ordre : le jeton contrôlé, l’appel, la panne qui laisse passer, l’erreur de votre appel qui refuse, success qui décide.

Connexion et inscription

Ces exemples conviennent aux formulaires à faible enjeu. Ils ne lient pas le jeton à un compte : ne les utilisez pas pour une connexion ou une inscription. Le SDK Node n’est pas encore publié ; le guide pas à pas dit ce que couvre cette documentation.

Remonter l’issue réelle d’une tentative (facultatif)

Antisy mesure ses règles contre ce qui est réellement arrivé : une inscription dont l’adresse est confirmée, une fraude que vous avez établie. Sans le kit Node, vous le dites avec le même secret que pour la vérification, en un appel. C’est de la mesure, jamais une décision : le signal ne produit aucun verdict, ne change rien en direct, et la réponse n’en porte aucun. Ne pas l’envoyer ne coûte rien à votre protection.

POST https://api.antisy.com/v1/feedback, sans en-tête Authorization (sa présence bascule vers le mode du SDK, signé et chiffré), avec un corps application/x-www-form-urlencoded ou application/json. Trois champs, et rien d’autre : un champ de plus est un 400 (bad-request).

secret
Le même secret de vérification que pour /v1/siteverify, dans le corps et nulle part ailleurs.
event_id
L’event_id que siteverify a rendu pour cette vérification (evt_ suivi de 26 caractères). Gardez-le avec l’objet qu’il décrit, la ligne de l’inscription ou du message : c’est la seule façon de signaler plus tard.
outcome
success, failure, account_confirmed_fraud ou false_positive. Les trois premiers décrivent la tentative ; false_positive dit que le verdict d’Antisy vous a paru faux, et se compte à part. N’envoyez que ce que vous savez : un compte dont l’adresse est confirmée, pas un compte seulement créé.
curl
# ANTISY_SECRET   : votre secret de vérification, dans l'environnement, jamais en dur.
# ANTISY_EVENT_ID : l'event_id que siteverify a rendu pour cette vérification.
# À lancer après avoir répondu à votre visiteur. Pas de -L : aucune redirection suivie.
printf 'secret=%s' "$ANTISY_SECRET" | curl -sS --max-time 3 -w '\n%{http_code}\n' \
  https://api.antisy.com/v1/feedback \
  -d @- \
  --data-urlencode event_id="$ANTISY_EVENT_ID" \
  --data-urlencode outcome=success

Réponse
{"success":true,"error-codes":[]}
200

Le corps de la réponse a deux clés, toujours : success et error-codes. Ici success dit seulement « signalement pris », pas un verdict ; il n’y a jamais de verdict, de score, de raison ni d’event_id. Les statuts sont de vrais statuts, à la différence de siteverify : un 4xx n’est jamais à réessayer, un 429 ou un 5xx l’est plus tard, une fois ou deux. Aucune décision ne dépend de la réponse : journalisez-la.

200
error-codes vide : pris, ou déjà pris à l’identique (même issue pour le même événement : rien n’est réécrit), ou mis de côté sans rien enregistrer (secret de test, site ou organisation suspendus).
401
missing-input-secret ou invalid-input-secret : secret absent ; inconnu, révoqué, d’un site fermé, ou clé d’évaluation (même réponse pour tous). Corrigez la configuration.
400 missing-input-event
event_id absent : corrigez l’appel.
400 invalid-input-event
event_id mal formé, ou événement que ce secret ne peut pas signaler (inexistant, d’un autre site, né d’une autre voie, ou sans enregistrement). Un seul refus pour tous ces cas. Ne réessayez pas.
400 missing-input-outcome
outcome absent : corrigez l’appel.
400 invalid-input-outcome
outcome hors de la liste : corrigez l’appel.
409 outcome-conflict
Une autre issue a déjà été signalée pour cet événement. Ne réessayez pas.
410 event-expired
L’événement a plus de 30 jours. Ne réessayez pas.
422 daily-limit-reached
Plafond quotidien de ce mode atteint. Ne réessayez pas ; reprenez demain (jour UTC) si l’issue compte pour vous.
400, 405, 413, 415
bad-request : champ inconnu (dont une note ou une date), corps illisible ou de plus de 16 Kio, méthode ou type de contenu non lu.
429, 5xx
rate-limited avec Retry-After, ou internal-error (503 avec Retry-After en cas de délestage). Réessayez plus tard.

Les limites

  • L’événement doit être né d’une vérification siteverify en mode simple de ce site, et avoir moins de 30 jours. Une seule issue par événement.
  • Certaines réponses de siteverify portent un event_id qui ne désigne aucun événement enregistré : secret refusé, site suspendu, jeton de test, corps de plus de 16 Kio, site Gratuit au-delà de son quota (over_quota dans flags). Les signaler donne invalid-input-event. Un événement de la voie signée n’est pas signalable ici.
  • Le mode a un plafond quotidien propre, par site (5 % des vérifications du jour, 20 au moins ; deux hypothèses, à revoir pendant le pilote), compté à part de celui du kit signé. Au-delà : 422. Le plafond ne laisse pas passer l’issue de chaque vérification : envoyez d’abord ce qui instruit le plus, failure et account_confirmed_fraud, puis success s’il reste de la place.
  • Le secret est celui de siteverify : un secret volé permettrait de fausser la mesure de vos événements des 30 derniers jours, et d’épuiser le plafond du jour de ce mode, donc d’empêcher vos vraies issues. Il ne change aucun verdict et ne lit rien. Le remplacer dans le tableau de bord le ferme.
  • Appelez la route depuis votre serveur, après avoir répondu au visiteur, avec un délai de 3 secondes et sans suivre de redirection ; un seul réessai, plus tard, sur une erreur réseau, un 429 ou un 5xx.

Limites

  • La route a ses propres plafonds de débit, par adresse et par clé. Au-delà : 429 et Retry-After. Ce statut peut être provoqué par le volume : voir plus haut.
  • Les secrets de test partagent 1 000 appels par jour pour toute l’organisation, tous sites confondus (jour UTC). Au-delà : 429 rate-limited, avec un Retry-After qui court jusqu’au prochain minuit UTC. Ils partagent aussi, à ce jour, 2 500 appels par mois (mois UTC) : au-delà, le même 429, dont le Retry-After court jusqu’au premier du mois UTC suivant. Voir les clés de test.
  • Un corps de plus de 16 Kio, ou un response de plus de 2048 caractères, reçoit la réponse d’un jeton forgé. Ne l’envoyez pas : contrôlez le jeton avant l’appel.