Une balise dans la page. À l’envoi d’un formulaire POST vers votre site, le script remplit lui-même le champ du jeton. Le visiteur ne voit rien : pas de case à cocher, pas d’énigme.
data-site est la clé de site, publique. Jamais un secret. Les clés n’ont pas de préfixe : rien dans la chaîne ne dit ce qu’elle est. Comptez les caractères avant de coller (32 pour la clé de site, 64 pour un secret).
Script classique : pas de type="module". defer ou async conviennent. Placez-le le plus tôt possible dans le <head>, une seule fois par page.
Chargez-le depuis api.antisy.com, sans le recopier chez vous : son contenu change à chaque livraison.
Pas d’attribut integrity. Une empreinte figée bloquerait le script à la livraison suivante, donc tous vos visiteurs.
Le script pose lui-même une empreinte sur la balise du script de mesure qu’il crée : votre navigateur ne l’exécute que s’il reçoit exactement les octets servis par Antisy. Rien à ajouter à votre politique CSP pour cela.
Ce que fait le script
À son chargement, le script installe window.antisy et un écouteur submit sur la fenêtre, puis prépare ses ressources pendant que le visiteur remplit le formulaire. Il compte aussi les interactions avec la page (page sur les données). Dès que le visiteur entre dans un formulaire (un clic ou une frappe dans un champ, le focus au clavier), il demande de lui-même un jeton de tentative et lance en arrière-plan un court calcul sur l’appareil, sans rien afficher. Ses écouteurs de préparation sont passifs : ils ne changent ni le focus ni le comportement de la page.
À l’envoi d’un formulaire couvert (section suivante), le script :
retient l’envoi le temps d’obtenir le jeton : vos gestionnaires ne voient pas encore l’événement ;
mesure le navigateur et dépose la mesure chez Antisy ;
met le jeton dans le champ caché antisy-token : il crée le champ s’il n’existe pas, et remplit tous ceux qui existent ;
reprend l’envoi par requestSubmit : vos gestionnaires voient un seul événement submit, champ déjà rempli.
L’attente dure 6 secondes au plus. Si Antisy ne répond pas ou si le délai est dépassé, le formulaire part quand même, sans jeton : le champ antisy-token est absent, ou vide s’il existait déjà dans le formulaire. La page suivante dit ce qui se passe alors. Les formulaires ajoutés à la page après son chargement sont couverts de la même façon. Votre serveur lit le champ antisy-token après l’analyse du corps, avec une valeur vide par défaut, et le fait vérifier (page sur la vérification).
Quels formulaires sont couverts
Par défaut, un formulaire dont la méthode est POST et dont l’adresse d’envoi est celle de la page : même origine, c’est-à-dire même schéma, même nom d’hôte et même port. La méthode est celle du bouton d’envoi s’il en porte une (formmethod), sinon celle du formulaire. L’adresse est le formaction du bouton s’il existe, sinon l’action du formulaire ; vide, c’est la page elle-même.
Un sous-domaine est une autre origine : un formulaire de www.exemple.ch qui envoie vers api.exemple.ch n’est pas couvert tant qu’il ne porte pas data-antisy. Aucun jeton ne part chez un tiers par accident.
Un formulaire de recherche (role="search", champ type="search") en POST vers votre site est couvert comme les autres, mais le script n’en prépare pas le jeton à l’avance : il le demande à l’envoi. En GET, il n’est pas couvert, sans rien à écrire.
Un formulaire method="dialog" ne l’est jamais.
Quatre attributs, tous facultatifs, se posent sur le <form> :
data-antisy="off"
Exclut le formulaire : le navigateur l’envoie tel quel, sans attente, sans champ, sans jeton. La valeur se lit sans tenir compte de la casse ni des blancs. Il l’emporte sur tout autre attribut.
data-antisy
Couvre un formulaire que le script laisse passer sinon : un GET, ou un POST vers une autre origine.
Pour un GET, le jeton part dans l’adresse : journaux du serveur et des intermédiaires, historique du navigateur, en-tête Referer. Il est à usage unique et expire vite, mais un rechargement le rejoue et la vérification le refuse. À réserver à un GET qui protège quelque chose, jamais à une recherche.
data-antisy-action="contact"
Nomme l’action déposée avec le jeton, rendue telle quelle par la vérification : de 1 à 32 caractères parmi a-z, 0-9, _ et -, en minuscules. Jamais de donnée personnelle. Sans cet attribut, le dépôt part sans action ; un nom invalide est ignoré.
data-antisy-ignore
L’ancien nom de l’exclusion, avec le même effet que data-antisy="off" : sa présence suffit, quelle que soit sa valeur.
Ce que le script ne couvre pas
Dans le doute, le script ne retient pas l’envoi : mieux vaut un formulaire sans jeton qu’un formulaire cassé. Ces envois partent tels quels, sans jeton ; la page qui veut les protéger appelle antisy.token() (section suivante).
Un formulaire dont la cible ouvre une autre fenêtre ou un cadre nommé (target, formtarget ou <base target> autre que vide ou _self ; _top et _parent valent _self dans une page de premier niveau). Retenir l’envoi plusieurs secondes ferait expirer l’activation du visiteur : le navigateur refuserait d’ouvrir la fenêtre, sans un message.
Un envoi par form.submit(), qui n’émet aucun événement submit (idem $(form).submit() de jQuery). form.requestSubmit() en émet un et est couvert.
Un envoi qui ne passe par aucun événement submit : un bouton type="button" dont le gestionnaire lit les champs, un fetch ou un XHR lancé par un autre événement.
Un GET ou un POST vers une autre origine non marqué data-antisy.
Les formulaires d’un composant web qui isole son DOM (l’événement submit n’en sort pas) et ceux d’une <iframe> : une balise par document.
Un navigateur sans requestSubmit (non vérifié : Safari avant 16).
Ce qui peut se perdre quand le script retient l’envoi
Un formulaire que votre page retire du DOM dans la fraction de seconde qui suit le clic ne part plus : l’envoi est perdu, sans message. Posez data-antisy="off" et appelez antisy.token() vous-même.
Un écouteur submit que votre page pose sur la fenêtre, en capture, avant la balise, voit l’événement retenu puis l’événement repris : un compteur d’envois compte deux envois, et une garde anti double envoi qui annule le second annulerait l’envoi repris. Posez-le sur document ou sur le formulaire.
Si votre code compare event.submitter à son bouton : quand le bouton a été retiré ou désactivé par un <fieldset> pendant l’attente, le script reprend l’envoi par un bouton temporaire caché, et c’est lui que vous voyez.
Un jeton ne sert qu’une fois. Après un envoi qui ne quitte pas la page (votre validation annule, un fetch reprend l’envoi), le prochain envoi fait par un geste du visiteur obtient un jeton neuf. Un réessai que votre script lance de lui-même, sans geste, repart avec un jeton déjà vérifié et sera refusé : demandez un jeton neuf à antisy.token().
Si l’API d’Antisy ne répond pas, chaque envoi est retenu jusqu’à 6 secondes avant de partir sans jeton. Pour un autre compromis, excluez le formulaire et appelez antisy.token() avec votre propre délai (totalTimeoutMs). Un délai court laisse moins de temps au calcul en arrière-plan : un appareil qui ne l’achève pas reçoit alors un jeton que la vérification refuse, et non null.
Demander un jeton soi-même : antisy.token()
Pour un envoi que le script ne couvre pas : fetch ou XHR, application à page unique, form.submit(), formulaire non couvert. La page appelle window.antisy.token(options), qui rend une promesse : le jeton, ou null. Elle ne lève jamais d’erreur, et se termine au plus tard après 5 secondes par défaut.
action
Une étiquette pour l’action : contact, newsletter… De 1 à 32 caractères parmi a-z, 0-9, _ et -. Antisy la rend telle quelle dans la réponse de la vérification. Jamais de donnée personnelle. Une étiquette qui ne respecte pas ce format fait échouer le dépôt : token() rend null.
totalTimeoutMs
Échéance de tout l’appel, en millisecondes. Par défaut 5000. À l’échéance, null.
Le dépôt se réserve 1 seconde sur ce délai : le calcul en arrière-plan s’arrête au plus tard 1 seconde avant l’échéance. S’il n’est pas fini, le jeton part sans sa solution et la vérification le refuse, quel que soit le mode. Plus le délai est court, plus un appareil lent obtient ainsi un jeton refusé plutôt que null.
Le jeton va dans l’en-tête x-antisy-token de votre requête, ou dans le champ antisy-token du formulaire. Le nom du champ et celui de l’en-tête font partie du contrat.
Application à page unique
async function sendContact(fields) {
// Mode explicite, pour un envoi par fetch que le script ne couvre pas : c'est votre page qui demande le jeton.
// Rend au plus tard après 5 s, sans jamais lever d'erreur.
const token = window.antisy ? await window.antisy.token({ action: "contact" }) : null;
return fetch("/api/contact", {
method: "POST",
headers: {
"content-type": "application/json",
...(token ? { "x-antisy-token": token } : {}),
},
body: JSON.stringify(fields),
});
}
Si la page appelle une autre origine que la sienne (www. vers api.), la réponse de votre serveur au contrôle CORS préalable doit autoriser l’en-tête : Access-Control-Allow-Headers: content-type, x-antisy-token. Sans cela, le navigateur refuse l’appel lui-même.
Un formulaire couvert envoyé par fetch
Un formulaire POST vers votre site que votre page envoie par fetch depuis un gestionnaire submit est couvert aussi : le script remplit le champ avant votre gestionnaire, qui peut le lire dans new FormData(form). Si vous posez l’en-tête vous-même, ajoutez data-antisy="off" au formulaire, pour n’avoir qu’un dépôt par envoi.
Si votre page appelait déjà token() pour un formulaire couvert
Les versions précédentes de cette documentation demandaient ce code. Il fonctionne toujours, mais il n’est plus nécessaire pour un formulaire POST vers votre site. Appelé pendant la distribution de l’événement submit repris, avec la même action que le formulaire (data-antisy-action, ou aucune des deux), il reçoit le jeton que le script vient de déposer. Sinon, il fait un second dépôt : deux dépôts pour le premier envoi de la page, qui devient plus lent. Pour migrer, retirez l’appel ; ou donnez au formulaire un data-antisy-action du même nom que l’appel ; ou posez data-antisy="off" si vous gardez la main. Un formulaire dont la page a appelé token() pendant la distribution de son événement est mené par la page : ses envois suivants ne sont plus retenus par le script, et un envoi dont votre gestionnaire ne remplit pas le champ part sans jeton.
Exclure un formulaire (facultatif)
Le script prépare le jeton dès que le visiteur entre dans un formulaire de la page, et retient l’envoi de tout formulaire couvert. Un formulaire que vous ne protégez pas (une lettre d’information présente sur toutes les pages, une déconnexion, une recherche en POST) le déclencherait pour chaque visiteur qui s’y arrête, et chaque jeton préparé compte dans la cadence d’émission de votre palier. Posez data-antisy="off" sur ce <form> :
L’attribut exclut le formulaire de la préparation comme de la reprise : ni attente, ni champ, ni jeton. Les champs et boutons rattachés à ce formulaire de l’extérieur par form="id" en sont exclus eux aussi.
Posé après coup par un script sur un formulaire que le script a déjà rempli, il laisse dans le formulaire le champ antisy-token avec son dernier jeton, déjà utilisé : l’envoi suivant le porterait et serait refusé. Retirez ce champ en même temps.
Il n’empêche pas d’appeler antisy.token() depuis ce formulaire : le jeton est alors demandé à l’appel.
Un formulaire vers une autre origine, non marqué, n’est pas retenu et ne reçoit aucun champ. Mais la préparation (un jeton de tentative, un tour de calcul) peut partir au contact d’un de ses champs : elle sert la page qui appelle antisy.token() pour ce formulaire, et le script ne peut pas savoir si c’est votre cas. Posez l’attribut si vous ne l’utilisez pas.
Politique CSP
Si votre site envoie une politique Content-Security-Policy, ajoutez ces sources à celles qu’elle porte déjà ('self' tient lieu des vôtres) :
La balise du script, et la balise du script de mesure que le script crée ensuite : même hôte. Cette seconde balise porte une empreinte que le script pose lui-même : aucune directive n’est à ajouter pour cette empreinte.
Absente : pas de window.antisy, ou pas de mesure, donc pas de jeton.
script-src 'wasm-unsafe-eval'
La compilation d’un module WebAssembly.
Absente : token() rend null. Le navigateur signale la violation (wasm-eval) ; la page ne lève aucune exception.
connect-src https://api.antisy.com
Les appels que le script fait à l’API : sa description, son module, le jeton de tentative et le dépôt. Le script de mesure n’en fait plus partie : une balise relève de script-src.
Absente : token() rend null.
worker-src blob: (facultative)
Le calcul en arrière-plan tourne dans un Worker créé depuis un blob:. Vous pouvez ajouter worker-src 'self' blob: à votre politique.
Absente, rien ne casse : le calcul se fait alors sur le fil principal de la page, par petites tranches, avec la même difficulté et le même résultat. Une politique qui ne cite pas blob: donne ce repli, pas une panne.
'unsafe-eval' n’est pas demandé
Le script est servi tel quel et ne fait évaluer aucun texte comme du code dans votre page. Une politique que vous aviez élargie pour une version précédente du script peut retirer cette source : seule 'wasm-unsafe-eval' reste, pour le module WebAssembly.
Politiques avec un nonce par réponse
Trois variantes fonctionnent, sans 'unsafe-inline' (<n> est le nonce de la réponse). Dans les trois cas, connect-src et worker-src restent ceux du bloc ci-dessus.
Sans liste d’hôtes : c’est le nonce de la balise du script qui décide. Elle le porte, et le script de mesure qu’elle crée est admis par 'strict-dynamic'.
Sans liste d’hôtes ni 'strict-dynamic' : le script recopie le nonce de sa propre balise sur celle du script de mesure, qui est donc admise.
Une politique à nonce qui contient 'strict-dynamic' ignore la liste d’hôtes de script-src (CSP de niveau 3) : le nonce de la balise du script décide.
Essayez d’abord la politique en Content-Security-Policy-Report-Only : les violations signalées nomment la directive à compléter.
Une politique qui n’a que default-src : ajoutez les directives ci-dessus explicitement plutôt que d’élargir default-src.
Trusted Types. Le script crée une politique nommée antisy, que la page doit autoriser si elle exige require-trusted-types-for 'script' : ajoutez antisy à votre directive trusted-types, à côté de vos propres politiques. Elle ne sert qu’à approuver les adresses que le script lui demande, et vos scripts ne peuvent pas s’en servir. Si la page charge la balise deux fois, ajoutez 'allow-duplicates'.En-tête HTTP
Vérifié dans Chrome ; non vérifié dans Firefox et Safari. Sans antisy dans votre liste, le navigateur refuse la balise du script de mesure, token() rend nullet la violation est signalée ; une politique default de votre page qui admet l’adresse rattrape ce cas. Une page qui n’exige rien mais dont la politique liste déjà des noms sans antisy voit le script fonctionner, avec une violation signalée à chaque page vue.
Noms d’hôte déclarés
Antisy ne délivre un jeton valable qu’aux pages dont le nom d’hôte figure exactement parmi ceux du site. Ils se déclarent dans l’onglet Paramètres du site : jusqu’à 20 noms, le nom seul, sans https://, sans port, sans chemin, sans joker. Déclarez-les avant de poser la balise, pas seulement avant d’allumer la collecte.
www.exemple.ch et exemple.ch sont deux noms. Déclarez chacun de ceux qui portent un formulaire.
Depuis un nom non déclaré, token() rend quand même une chaîne qui a l’air d’un jeton : le navigateur n’apprend rien. C’est la vérification qui la refuse (success à false), que la collecte soit allumée ou éteinte : dès que la balise est posée, chaque jeton venu d’une page servie sous ce nom est refusé.
Pour développer en local, mettez plutôt une clé de site factice dans data-site (clés de test) : elle n’a pas de noms d’hôte, la page locale reçoit le jeton fixe de cette clé, que votre serveur vérifie avec un secret de test. Votre site de production garde ainsi sa liste de noms, sans localhost : un nom déclaré vaut pour toute page servie sous ce nom, et localhost désigne la machine de chacun, pas seulement la vôtre.
Le champ hostname de la réponse ne porte le nom de la page que si le navigateur a fourni une origine exploitable. Depuis un iframe sandbox, l’origine est null : le jeton est bien délivré, mais hostname vaut null.
HTTPS et navigateurs
Le script a besoin d’un contexte sécurisé : HTTPS, ou localhost en développement. Sur une page en HTTP, token() rend null et le script ne retient plus aucun envoi, sauf le tout premier s’il part avant le chargement du script de mesure : celui-là peut encore être retenu un court instant, puis il part sans jeton.
Il scelle sa mesure avec l’algorithme X25519. Quand le navigateur ne l’a pas dans Web Crypto, le script fait lui-même le calcul : un repli écrit par Antisy, qui produit la même enveloppe et suit les mêmes règles, et qui exige BigInt. Un navigateur qui n’a ni X25519 ni BigInt n’obtient pas de jeton.
Mesuré par Antisy, sur le parcours complet (jeton, puis vérification côté serveur) : Chrome, Firefox (version non consignée) et Safari 17.5 sur macOS 14.5. Safari 17.5 n’a pas X25519 dans Web Crypto (mesuré le 4 octobre 2026) : il passe par le repli.
Non mesuré : iPhone et iPad, les autres versions de Safari et de Firefox, Edge, les navigateurs de téléphone. D’après MDN (consulté par Antisy le 23 septembre 2026, non revérifié), Chrome avant la version 133 et Firefox avant 130 n’ont pas X25519 : le repli s’applique à eux comme à Safari, mais aucune de ces versions n’a été mesurée.
Si le script ne tourne pas
Une politique CSP qui refuse script-src pour l’hôte de l’API ou 'wasm-unsafe-eval', ou connect-src pour l’API, un bloqueur de publicité ou de traceurs, une page en HTTP, un navigateur sans X25519 ni BigInt, une API injoignable depuis le navigateur, un intermédiaire qui modifie les scripts en chemin (un proxy d’entreprise qui y injecte du code, par exemple : l’empreinte du script de mesure ne correspond plus, le navigateur refuse de l’exécuter et token() rend null), un visiteur sans JavaScript, un formulaire que le script ne couvre pas et dont la page n’appelle pas token() (form.submit(), fetch, cible dans une autre fenêtre : voir « Quels formulaires sont couverts ») : dans tous ces cas, le formulaire part sans jeton : le champ antisy-token est absent du corps, ou vide s’il existait déjà dans le formulaire. Quand le script a retenu l’envoi et que le jeton échoue ou dépasse 6 secondes, il part de même. Lisez donc le champ avec une valeur vide par défaut, sans exiger sa présence : votre serveur appelle alors la vérification avec un jeton vide.
Ce qui suit dépend du réglage « Collecte du navigateur », dans l’onglet Paramètres du site :
Collecte allumée
Un envoi sans jeton ne passe pas, dans aucun mode de vérification : verdict deny, success à false, error-codes à ["missing-input-response"]. Le visiteur ne le sait pas ; votre serveur, si.
Une politique CSP incomplète, ou un formulaire non couvert dont la page oublie token(), fait donc refuser tous vos visiteurs, humains compris.
Collecte éteinte
L’absence de jeton n’est pas reprochée : le verdict suit le reste de l’évaluation, et rend allow si rien d’autre ne retient le visiteur. Un jeton présent mais invalide, rejoué, expiré ou d’un autre site est, lui, refusé que la collecte soit allumée ou éteinte. Un site neuf naît avec la collecte éteinte.
Un jeton présent n’est pas toujours accepté : un appareil qui n’achève pas à temps le calcul en arrière-plan (onglet caché, appareil lent) envoie sans la solution, et la tentative est refusée comme un jeton invalide, quel que soit le mode, sans que la réponse en donne la cause. Prévoyez pour tout refus le même message au visiteur (« réessayez ») : un nouvel essai tire un nouveau calcul avec un jeton neuf.
Avant d’allumer la collecte
La balise est dans le <head> de chaque page qui porte un formulaire, en HTTPS, et la CSP autorise le script et les appels vers Antisy.
Chaque nom d’hôte de ces pages est déclaré sur le site : c’était à faire avant de poser la balise (noms d’hôte déclarés).
Chaque formulaire POST vers votre site est couvert tel quel, ou exclu en connaissance de cause (data-antisy="off") ; un formulaire vers une autre origine, sous-domaine compris, que vous voulez protéger porte data-antisy.
Chaque envoi que le script ne couvre pas (fetch, form.submit(), cible dans une autre fenêtre) appelle antisy.token() et pose le jeton dans antisy-token ou x-antisy-token.
Votre serveur lit ce jeton et le vérifie, en laissant passer l’envoi si l’API est injoignable.
Si vos visiteurs sont refusés à tort, vérifiez d’abord les noms d’hôte déclarés. Éteindre la collecte depuis le tableau de bord ne rattrape que les envois sans jeton, et ne change rien d’autre : un refus avec un jeton présent (nom d’hôte non déclaré, calcul inachevé sur un appareil lent) n’en dépend pas.