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êtex-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éponse401, dans un corps d’une autre forme. Un secret envoyé enAuthorization: Bearerest 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
{
"success": true,
"verdict": "allow",
"flags": [],
"error-codes": [],
"challenge_ts": "2026-09-24T10:00:00Z",
"hostname": "shop.example",
"action": "contact",
"event_id": "evt_01J7X4M2Q9K8ZP3R5T6V7W8Y9A"
}- success
truesi et seulement si le visiteur est accepté. C’est le champ à lire si vous ne voulez qu’un booléen.- verdict
allowoudeny, et rien d’autre.successvauttruesi et seulement siverdictvautallow: 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
allowqui a un dépôt derrière lui. Unallowsans 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 (avecinternal-error). Ne le rendez donc pas obligatoire pour accepter un visiteur.nullaussi 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.nullsinon, et sur tout refus. - action
- Sur un
allow, l’étiquette passée àtoken()ou posée pardata-antisy-actionsur le formulaire, ounulls’il n’y en avait pas.nullaussi 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
allowsuccessàtrue. Publiez ou enregistrez le message.denysuccessà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
totalTimeoutMstrop 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àfalseettimeout-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.
# 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
doneCodes 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 :
{ "success": false, "error-codes": ["rate-limited"] }missing-input-secretStatut 200Le 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 200Le 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 200Le 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 200Le 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 200Le 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, 415Requê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 429Trop 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, 503Panne 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.
- 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.
- 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. - Ne laissez jamais passer sur
400,401,413,415ni sur aucun autre statut 4xx à part le429. C’est un défaut de votre appel : journalisez-le comme un bug à corriger, et traitez l’envoi comme non vérifié : refusez-le. - Le 429 est un choix.
Retry-Afterdonne le délai en secondes. Avec un secret de test, un429peut 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 un429par 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 un429(deny) : c’est le choix protecteur, au prix de refuser des visiteurs légitimes pendant une saturation, qui réessaieront aprèsRetry-After. Le comportement par défaut est réglé par une constante dans l’exemple Node. Un503qui porteRetry-Afterest un délestage : l’instance a atteint sa capacité d’accueil, et un tiers peut le provoquer par le volume, comme le429. Les exemples le traitent comme une panne ; pour un formulaire sensible, refusez aussi sur ce503, avec la même valeur que pour le429. - Un secret absent ou refusé ne relève pas de cette règle : la réponse est un refus à
200(deny). Journalisezmissing-input-secretetinvalid-input-secretcomme 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
- 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.
- Il appelle
POST /v1/siteverify, avec un corps de formulaire qui portesecretetresponse, et un délai de 3 secondes. Le secret vient de la variable d’environnementANTISY_SECRET: il n’est jamais en dur, jamais en argument de ligne de commande, jamais dans un en-tête. - Il ne lit que
success:trueest la seule acceptation. Une réponse illisible est un envoi non vérifié, donc refusé. - Il laisse passer seulement quand Antisy ne répond pas : erreur réseau, délai dépassé, réponse
5xx. Il journalise chaque fois. - Il ne laisse jamais passer sur une erreur de votre appel :
400,401,413,415et tout autre4xx, ni sur une redirection3xx, 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.
<?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 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.
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 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 pourquoiGo
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.
// 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 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 portesecretetresponse, 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,
successqui 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_idquesiteverifya 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_fraudoufalse_positive. Les trois premiers décrivent la tentative ;false_positivedit 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éé.
# 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{"success":true,"error-codes":[]}
200Le 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.
200error-codesvide : 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).401missing-input-secretouinvalid-input-secret: secret absent ; inconnu, révoqué, d’un site fermé, ou clé d’évaluation (même réponse pour tous). Corrigez la configuration.400missing-input-event- event_id absent : corrigez l’appel.
400invalid-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.
400missing-input-outcome- outcome absent : corrigez l’appel.
400invalid-input-outcome- outcome hors de la liste : corrigez l’appel.
409outcome-conflict- Une autre issue a déjà été signalée pour cet événement. Ne réessayez pas.
410event-expired- L’événement a plus de 30 jours. Ne réessayez pas.
422daily-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,415bad-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,5xxrate-limitedavecRetry-After, ouinternal-error(503avecRetry-Afteren cas de délestage). Réessayez plus tard.
Les limites
- L’événement doit être né d’une vérification
siteverifyen mode simple de ce site, et avoir moins de 30 jours. Une seule issue par événement. - Certaines réponses de
siteverifyportent unevent_idqui 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_quotadansflags). Les signaler donneinvalid-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,failureetaccount_confirmed_fraud, puissuccesss’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
429ou un5xx.
Limites
- La route a ses propres plafonds de débit, par adresse et par clé. Au-delà :
429etRetry-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à :
429rate-limited, avec unRetry-Afterqui court jusqu’au prochain minuit UTC. Ils partagent aussi, à ce jour, 2 500 appels par mois (mois UTC) : au-delà, le même429, dont leRetry-Aftercourt jusqu’au premier du mois UTC suivant. Voir les clés de test. - Un corps de plus de 16 Kio, ou un
responsede 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.