# Comment ajouter Cloudflare Turnstile à un formulaire HTML

> Ajoutez Cloudflare Turnstile à un formulaire HTML simple : le widget, siteverify côté serveur, les clés de test, et une façon de l'utiliser sans clés ni serveur à vous.

Publié le 2026-10-25 · https://ikiform.com/fr/guides/spam-protection/cloudflare-turnstile

Ajoutez le script de Turnstile et un élément `cf-turnstile` dans votre formulaire ; le widget place un token dans un champ masqué `cf-turnstile-response`. Envoyez ensuite ce token et votre clé secrète à l'endpoint siteverify de Cloudflare depuis un serveur, et n'acceptez l'envoi que s'il répond `success`. Sans serveur, laissez votre backend de formulaires le vérifier, ou utilisez la vérification gérée d'Ikiform, qui ne demande aucune clé.

## Ce qui change avec Turnstile

Turnstile a deux moitiés, et un formulaire HTML simple n'a de place que pour une. Le widget tourne dans le navigateur avec votre **site key** publique et produit un token. La vérification tourne sur un serveur avec votre **secret key**, qui ne doit jamais atteindre la page. Un formulaire qui affiche le widget sans jamais vérifier le token n'est que décoration : un bot envoie directement vers votre endpoint et ignore complètement le widget.

La vraie question pour un formulaire HTML est donc de savoir où vit le secret. Cette page couvre trois réponses : votre propre petit serveur, un backend de formulaires qui détient vos clés, et une vérification gérée où personne d'autre que le backend ne détient de clés.

Schéma : La vie d'un token Turnstile, du widget au verdict

1. Le widget Turnstile tourne dans le navigateur avec votre site key publique. La plupart des visiteurs ne le voient jamais rien faire.
2. Il émet un token et le place dans un input masqué cf-turnstile-response à l'intérieur du formulaire.
3. Le formulaire est envoyé, et le token voyage avec les autres champs.
4. Votre serveur envoie la clé secrète, le token et l'IP du visiteur à l'endpoint siteverify de Cloudflare.
5. Il vérifie que la réponse indique success et que le hostname et l'action sont ceux attendus.
6. Si tout correspond, l'envoi est accepté. Si quelque chose cloche, il est refusé.

Un token est valable 300 secondes et ne peut être vérifié qu'une fois. Une seconde vérification du même token échoue avec timeout-or-duplicate.

## Ajouter le widget

Créez un widget dans le tableau de bord Cloudflare sous **Turnstile**, listez les hostnames où tourne votre formulaire, et copiez la site key. Ajoutez ensuite le script et un élément `cf-turnstile` dans le formulaire. L'élément doit être à l'intérieur du `<form>`, sinon le champ masqué du token se retrouve en dehors et n'est jamais envoyé.

`contact.html`:

```html
<form action="https://example.com/api/contact" method="POST">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" required />
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <label for="message">Message</label>
  <textarea id="message" name="message" rows="5" required></textarea>
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY" data-action="contact"></div>
  <button>Send message</button>
</form>

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
```

- `data-action="contact"` étiquette le widget. Siteverify le renvoie : votre serveur peut donc refuser un token émis pour un autre formulaire.
- Le script doit être chargé depuis l'URL de Cloudflare. D'après Cloudflare, une copie passée par un proxy ou auto-hébergée casse le widget.
- Si votre site envoie une Content Security Policy, autorisez `https://challenges.cloudflare.com` dans `script-src` et dans `frame-src`.
- Sans frais, Cloudflare autorise jusqu'à 20 widgets, chacun sur jusqu'à 10 hostnames : listez donc aussi les hôtes de préproduction.

## Vérifier le token sur votre propre serveur

N'importe quel serveur peut vérifier un token : un `POST` vers siteverify avec le secret et le token. Le voici sous forme de Cloudflare Worker vers lequel un formulaire statique envoie. Il vérifie le token, le hostname et l'action, retire le token, et transmet les champs à Ikiform avec une clé secrète. Remplacez cette transmission par tout autre moyen de stocker ou d'envoyer le message par e-mail.

`src/index.ts`:

```ts
interface Env {
  TURNSTILE_SECRET_KEY: string;
  IKIFORM_SECRET_KEY: string;
}

interface Verdict {
  success: boolean;
  hostname?: string;
  action?: string;
  "error-codes"?: string[];
}

const SITE = "https://example.com";
const SITEVERIFY = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method !== "POST") return new Response("Method not allowed", { status: 405 });
    const form = await request.formData();

    const check = await fetch(SITEVERIFY, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        secret: env.TURNSTILE_SECRET_KEY,
        response: String(form.get("cf-turnstile-response") ?? ""),
        remoteip: request.headers.get("CF-Connecting-IP"),
        idempotency_key: crypto.randomUUID(),
      }),
    });
    const verdict = (await check.json()) as Verdict;
    const expected = verdict.hostname === new URL(SITE).hostname && verdict.action === "contact";
    if (!verdict.success || !expected) {
      console.warn("turnstile", verdict["error-codes"] ?? [], verdict.hostname, verdict.action);
      return Response.redirect(`${SITE}/contact?error=check`, 303);
    }

    form.delete("cf-turnstile-response");
    const sent = await fetch(ENDPOINT, {
      method: "POST",
      headers: {
        Accept: "application/json",
        Authorization: `Bearer ${env.IKIFORM_SECRET_KEY}`,
        Origin: request.headers.get("Origin") ?? SITE,
      },
      body: form,
    });
    return Response.redirect(sent.ok ? `${SITE}/thanks` : `${SITE}/contact?error=send`, 303);
  },
};
```

- Siteverify accepte des corps JSON ou form-encoded et répond toujours en JSON. `remoteip` est facultatif ; `idempotency_key` permet de relancer une vérification sans risque après une erreur réseau.
- Vérifiez `hostname` et `action`, pas seulement `success`. Un token émis sur la page de quelqu'un d'autre avec votre site key indique aussi `success`.
- Stockez les deux secrets avec `wrangler secret put`, jamais dans `wrangler.toml`.
- Le Worker répond par une redirection `303` : le formulaire simple n'a donc besoin d'aucun JavaScript. Faites pointer l'`action` du formulaire vers la route du Worker.
- Il transmet l'en-tête `Origin` de la page, car un fetch côté serveur n'en envoie aucun et Ikiform le lit pour reconnaître les tests gratuits depuis `localhost`.

Pour afficher l'erreur sur la page du formulaire après un échec de vérification, lisez la query string que définit le Worker :

`contact.html`:

```html
<p id="form-error" role="alert" hidden></p>
<script>
  const reason = new URLSearchParams(location.search).get("error");
  const banner = document.getElementById("form-error");
  if (reason && banner) {
    banner.textContent =
      reason === "check" ? "The human check didn't pass. Try again." : "Your message didn't send. Try again.";
    banner.hidden = false;
  }
</script>
```

> Les versions côté serveur envoient avec une clé secrète, donc Ikiform ignore **Allowed domains** et sa vérification humaine. Chaque envoi vient alors de l'IP de votre serveur, et la limite Standard de 5 par minute et par visiteur compte tous vos visiteurs ensemble. Choisissez la [limite de débit](https://ikiform.com/fr/docs/rate-limits.md) Relaxed ou Custom pour un formulaire que vous envoyez via un serveur, et gardez `IKIFORM_SECRET_KEY` dans des variables d'environnement réservées au serveur.

## Laisser Ikiform le vérifier

Si vous ne voulez faire tourner aucun serveur, le formulaire peut envoyer directement à Ikiform, qui fait l'appel à siteverify. Le réglage **Spam check** de chaque formulaire décide comment.

| Chemin | Clés que vous gérez | Widget sur votre page | Serveur à vous |
| --- | --- | --- | --- |
| Votre propre serveur ou Worker, ci-dessus | Site key et clé secrète | Oui, le vôtre | Oui |
| Ikiform avec **Your own Turnstile keys** | Site key et clé secrète, collées dans Ikiform | Oui, le vôtre | Non |
| Ikiform avec **Ikiform** | Aucune | Non. Une page de vérification sur ikiform.com, ou un petit cadre | Non |

Avec **Your own Turnstile keys**, collez la site key et la clé secrète dans les réglages du formulaire. Le secret est chiffré au repos. Gardez le même balisage du widget, mais faites pointer le formulaire vers votre endpoint Ikiform. Ikiform vérifie `cf-turnstile-response` et compare le hostname du token aux **Allowed domains** du formulaire. Un envoi sans token valide reçoit `403 captcha_failed`, même avec une clé API secrète.

`contact.html`:

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" required />
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />
  <label for="message">Message</label>
  <textarea id="message" name="message" rows="5" required></textarea>
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY" data-action="contact"></div>
  <button>Send message</button>
</form>

<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
```

Avec **Ikiform**, vous n'ajoutez rien à la page. Un envoi HTML simple sans token reçoit une courte page **Checking you're human** sur ikiform.com qui exécute Turnstile et transmet les champs. Un envoi `fetch` reçoit plutôt `403 challenge_required` ; appelez d'abord `challenge()` de `@ikiform/sdk` pour obtenir un token. Aucun compte Cloudflare n'entre en jeu. La [documentation sur le spam](https://ikiform.com/fr/docs/spam.md#human-check) montre les deux parcours.

Si Cloudflare est injoignable, Ikiform ne jette pas l'envoi : il le stocke comme spam avec la raison `captcha_unavailable`, pour que vous puissiez le restaurer depuis le tableau de bord. Votre propre serveur doit faire le même choix délibérément, soit en échouant de manière fermée comme le Worker ci-dessus, soit en gardant l'envoi pour relecture.

## Clés de test

Cloudflare publie des clés factices qui fonctionnent sur n'importe quel hostname, y compris `localhost`. Utilisez-les pendant le développement, puis remplacez-les par vos vraies clés.

| Clé | Valeur | Comportement |
| --- | --- | --- |
| Site key | `1x00000000000000000000AA` | Passe toujours, widget visible |
| Clé secrète | `1x0000000000000000000000000000000AA` | Siteverify répond toujours success |
| Clé secrète | `2x0000000000000000000000000000000AA` | Siteverify répond toujours un échec |

Les tokens de test ne portent pas votre vrai hostname : la vérification du hostname du Worker les refuse donc. N'assouplissez cette vérification que dans votre environnement de développement, jamais en production.

## Vérifiez que tout fonctionne

1. Envoyez le formulaire normalement. L'envoi est accepté et le widget n'a rien affiché, ou une simple case à cocher.
2. Retirez l'élément `cf-turnstile` et envoyez. L'envoi est refusé : le Worker redirige avec `?error=check`, ou Ikiform répond `403 captcha_failed`.
3. Envoyez, revenez en arrière et renvoyez la même page. Le token réutilisé échoue, ce qui montre que le serveur le vérifie vraiment.
4. Ouvrez **Submissions** dans le tableau de bord. La nouvelle soumission y est, avec tous les champs envoyés.
5. Rien n'est arrivé ? Passez le filtre de statut du tableau de bord sur **Spam**. Le spam reçoit la même réponse de succès qu'une vraie soumission.

## Pièges

- **Un token, un essai.** Après un envoi échoué géré avec `fetch`, appelez `turnstile.reset()` pour obtenir un nouveau token. Un rechargement complet de la page en obtient un automatiquement.
- **Les personnes qui tapent lentement.** Les tokens expirent après 300 secondes. Le réglage `data-refresh-expired` du widget décide s'il en récupère un nouveau tout seul ou s'il attend un clic.
- **www et le domaine nu.** `www.example.com` et `example.com` sont deux hostnames différents pour siteverify. Comparez avec celui où tourne réellement votre formulaire, ou autorisez les deux.
- **Navigation monopage.** Le script implicite rend les widgets une seule fois, à son chargement. Lors d'un changement de route côté client, faites un rendu explicite avec `?render=explicit` et `turnstile.render()`.
- **Envois serveur avec les clés d'Ikiform.** Si votre serveur transmet à Ikiform avec une clé secrète, Ikiform saute sa vérification gérée. Vérifiez d'abord le token vous-même, comme le fait le Worker.

## Questions

### Puis-je utiliser Cloudflare Turnstile sans serveur ?

Le widget tourne n'importe où, mais le token doit être vérifié avec votre clé secrète sur un serveur. Sans serveur, envoyez le formulaire à un backend de formulaires qui le vérifie pour vous, ou utilisez la vérification gérée d'Ikiform, qui ne demande aucune clé.

### Cloudflare Turnstile est-il gratuit ?

Cloudflare le propose sans frais pour jusqu'à 20 widgets avec jusqu'à 10 hostnames chacun, et sa page des offres annonce des défis illimités. Consultez la page actuelle des offres de Cloudflare avant de vous y fier.

### Pourquoi siteverify répond-il timeout-or-duplicate ?

Le token a plus de 300 secondes ou a déjà été vérifié une fois. Affichez un nouveau widget, ou réinitialisez-le, après chaque tentative d'envoi.

### Ai-je besoin d'un compte Cloudflare pour utiliser Turnstile avec Ikiform ?

Non. Réglez le Spam check du formulaire sur Ikiform et Ikiform exécute Turnstile sur ikiform.com. Il ne vous faut un compte que pour utiliser vos propres clés et votre propre widget.

## Autres guides

- [Arrêter le spam des formulaires de contact](https://ikiform.com/fr/guides/spam-protection.md): Arrêtez le spam des formulaires de contact avec des couches invisibles pour les vrais visiteurs : honeypot, délai, score du contenu, limites de débit, vérification humaine si besoin.
- [Alternatives à reCAPTCHA](https://ikiform.com/fr/guides/spam-protection/recaptcha-alternatives.md): Comparez les alternatives à reCAPTCHA pour formulaires, de Turnstile et hCaptcha à la preuve de travail ou à l'absence de captcha, puis migrez avec un petit changement sûr.
- [Sites statiques](https://ikiform.com/fr/guides/spam-protection/static-site.md): Arrêtez le spam d'un formulaire de site statique, même sans secret ni compteur, avec un partial honeypot, un script sûr et des vérifications côté serveur.
- [Sans captcha](https://ikiform.com/fr/guides/spam-protection/without-captcha.md): Arrêtez l'essentiel du spam sans captcha : un honeypot, une heure de rendu signée, un score du contenu et une limite de débit, cumulés plutôt qu'utilisés seuls.
- [Champ honeypot](https://ikiform.com/fr/guides/spam-protection/honeypot.md): Ajoutez à n'importe quel formulaire un honeypot qui attrape les bots sans gêner lecteurs d'écran, clavier ou remplissage automatique, avec la vérification serveur pour chaque stack.
- [Limitation de débit](https://ikiform.com/fr/guides/spam-protection/rate-limiting.md): Limitez les envois de formulaire par visiteur avec un stockage partagé, répondez 429 avec Retry-After, et évitez de bloquer les bureaux qui partagent une IP.
