# Vérifier les signatures des webhooks de formulaire

> Vérifiez qu'un webhook de formulaire vient bien d'Ikiform : le schéma HMAC-SHA256 de Standard Webhooks, un vecteur de test, les corps bruts, les timestamps et la rotation du secret.

Publié le 2026-10-25 · https://ikiform.com/fr/guides/webhook-signature-verification

Recalculez vous-même la signature et comparez-la à celle de la requête. Ikiform suit Standard Webhooks : la signature est un HMAC-SHA256 en base64 de `webhook-id`, `webhook-timestamp` et du corps brut joints par des points, avec pour clé la partie décodée en base64 de votre secret `whsec_`, et envoyé sous la forme `v1,<signature>`. N'acceptez la requête que si une signature `v1` correspond lors d'une comparaison en temps constant et que le timestamp date de moins de 5 minutes.

Un endpoint de webhook est une URL publique qui accepte les requêtes POST de n'importe qui. Sans vérification de signature, quiconque découvre l'URL peut injecter de fausses soumissions dans votre CRM, votre base de données ou le chat de votre équipe. La vérification tient en quelques lignes de code, et ce guide porte justement sur l'art d'écrire ces lignes exactement comme il faut.

## Comment fonctionne la signature

Ikiform et votre serveur partagent un secret, affiché une seule fois quand vous ajoutez l'endpoint. Avant chaque tentative de livraison, Ikiform hache la requête avec ce secret et place le résultat dans un en-tête. Votre serveur refait le calcul sur ce qu'il a reçu. Seul quelqu'un qui détient le secret peut produire une valeur correspondante, et toute modification du corps, de l'identifiant ou du timestamp la casse.

Schéma : Comment Ikiform signe une livraison de webhook et comment votre serveur la vérifie

1. Ikiform sérialise l'événement en JSON.
2. Il joint l'identifiant de livraison, le timestamp Unix de la tentative et le corps avec des points.
3. Il calcule un HMAC-SHA256 de cette chaîne avec chaque secret actif de l'endpoint.
4. Il envoie chaque résultat, encodé en base64 et préfixé par v1, dans l'en-tête webhook-signature.
5. Votre gestionnaire lit le corps brut et les trois en-têtes avant d'analyser quoi que ce soit.
6. Il calcule le même HMAC avec sa copie du secret.
7. Il compare en temps constant avec chaque signature v1 et vérifie que le timestamp date de moins de 5 minutes.
8. S'il y a correspondance, il répond 2xx et traite l'événement. Sinon, il répond 400 et ne fait rien.

Rien de secret ne voyage avec la requête. La signature prouve que l'expéditeur connaissait le secret, et que les octets n'ont pas été modifiés en chemin.

## Le schéma, précisément

Ikiform utilise [Standard Webhooks](https://www.standardwebhooks.com), une spécification ouverte que partagent plusieurs fournisseurs de webhooks. Chaque livraison porte trois en-têtes :

| En-tête | Valeur | Remarques |
| --- | --- | --- |
| `webhook-id` | `dlv_...` | L'identifiant de livraison. Identique à chaque nouvelle tentative, il sert donc aussi de clé d'idempotence |
| `webhook-timestamp` | Secondes Unix, comme `1792300000` | Le moment où cette tentative a été signée. Nouveau à chaque nouvelle tentative |
| `webhook-signature` | `v1,` suivi de base64 | Une ou plusieurs valeurs séparées par des espaces. Deux pendant la rotation d'un secret |

`Ce qui est signé`:

```text
webhook-id:        dlv_01k7v2d4n8r2p6s0w3y5b7c9e1
webhook-timestamp: 1792300000
raw body:          {"id":"dlv_01k7...","type":"endpoint.test",...}

signed content = webhook-id + "." + webhook-timestamp + "." + raw body
key            = base64-decode(secret without "whsec_")
signature      = base64(HMAC-SHA256(key, signed content))
header         = "v1," + signature
```

Pour vérifier une livraison, dans cet ordre :

1. Lisez le corps brut en octets ou en chaîne exacte. Ne l'analysez pas encore.
2. Lisez les trois en-têtes. S'il en manque un, rejetez.
3. Vérifiez que le timestamp est un entier à moins de 5 minutes de votre horloge, dans un sens ou dans l'autre. Sinon, rejetez.
4. Retirez `whsec_` de votre secret et décodez le reste en base64. Ces 32 octets sont la clé HMAC, pas la chaîne elle-même.
5. Calculez le HMAC-SHA256 de `{webhook-id}.{webhook-timestamp}.{raw body}` et encodez-le en base64 avec l'alphabet standard et le padding.
6. Découpez `webhook-signature` sur les espaces. Pour chaque entrée, découpez sur la première virgule ; ignorez tout ce dont la version n'est pas `v1`.
7. Comparez chaque valeur `v1` à la vôtre en temps constant. Si l'une correspond, la livraison est authentique.
8. Seulement maintenant, analysez le JSON et agissez en conséquence.

## Un vecteur de test

Avant de déployer un gestionnaire, prouvez qu'il calcule la bonne valeur. Ces entrées sont un vrai corps de livraison `endpoint.test` signé avec un secret inventé. Votre code doit produire exactement la signature de la dernière ligne. Chaque guide par langage inclut un test unitaire qui les utilise.

| Entrée | Valeur |
| --- | --- |
| Secret | `whsec_Fg8c6hm1Gt41v5RSPKm72LggDx8S1mlWkbBYpHPAnpw=` |
| `webhook-id` | `dlv_01k7v2d4n8r2p6s0w3y5b7c9e1` |
| `webhook-timestamp` | `1792300000` |
| Corps brut | Les octets exacts du script ci-dessous, sans espaces ni saut de ligne |
| `webhook-signature` attendu | `v1,Duq+Iuh0xbRZyS+C/TO5W1hcGLGwg+ifBiWiYkq15qA=` |

Vous pouvez le vérifier sans écrire de code. Cette commande s'exécute dans un terminal sous macOS ou Linux et affiche la partie base64 de la signature :

`Terminal`:

```bash
SECRET='whsec_Fg8c6hm1Gt41v5RSPKm72LggDx8S1mlWkbBYpHPAnpw='
ID='dlv_01k7v2d4n8r2p6s0w3y5b7c9e1'
TS='1792300000'
BODY='{"id":"dlv_01k7v2d4n8r2p6s0w3y5b7c9e1","type":"endpoint.test","timestamp":"2026-10-17T12:00:00.000Z","api_version":"2026-10-17","data":{"endpoint_id":"whk_01k7v2e1b3d5f7h9k1m3n5q7s9","message":"This is a test event from Ikiform."}}'

KEY=$(printf '%s' "${SECRET#whsec_}" | base64 -d | xxd -p -c 256)
printf '%s.%s.%s' "$ID" "$TS" "$BODY" \
  | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$KEY" -binary \
  | base64
```

Deux autres cas méritent chacun un test, car ce sont les deux causes d'échec réelles les plus fréquentes :

| Modification | Signature | Votre gestionnaire doit-il l'accepter ? |
| --- | --- | --- |
| Le même corps mis en forme avec une indentation de deux espaces | `v1,se0TDhknrG0hYMvCId83HnpZfJcELG3ozxFJktRUHqY=` | Non. Même JSON, octets différents |
| `webhook-timestamp` une seconde plus tard | `v1,oBfWyLDPfrwVSo40e6b7B7+LmKkZX7OqT88SsrpY6ks=` | Non. Le timestamp est signé |
| Signé avec un second secret pendant une rotation | `v1,gfHP3s7ANdIpnM7gewtWtSvTrHgmsbEFgCaeRRR0Eak=` | Oui, si l'en-tête contient aussi une valeur qui correspond à votre secret |

Pendant une rotation de secret, l'en-tête porte les deux valeurs. Votre gestionnaire, avec l'un ou l'autre secret, doit accepter ceci :

`Deux signatures`:

```http
webhook-id: dlv_01k7v2d4n8r2p6s0w3y5b7c9e1
webhook-timestamp: 1792300000
webhook-signature: v1,Duq+Iuh0xbRZyS+C/TO5W1hcGLGwg+ifBiWiYkq15qA= v1,gfHP3s7ANdIpnM7gewtWtSvTrHgmsbEFgCaeRRR0Eak=
```

> Le timestamp du vecteur est fixe, donc un vrai vérificateur le rejette comme trop ancien. Les tests doivent passer une heure courante fixe, ce que font les guides par langage. Le [vérificateur de signature](https://ikiform.com/fr/docs/webhooks.md#verifier) de la page sur les webhooks vérifie dans le navigateur n'importe quels secret, en-têtes et corps.

## Façons de vérifier

| Approche | Points forts | Compromis |
| --- | --- | --- |
| `verifyWebhook` de `@ikiform/sdk/webhooks` | Un seul appel qui vérifie tout et renvoie un événement typé. Fonctionne sur Node, Workers, Deno et Bun | JavaScript et TypeScript uniquement |
| Une bibliothèque Standard Webhooks | Du code maintenu pour Python, Go, PHP, Ruby, JavaScript, Rust, Java, C# et Elixir | Une dépendance de plus. Vérifiez comment chacune attend les en-têtes ; certaines sont sensibles à la casse |
| Votre propre code HMAC | Aucune dépendance, une vingtaine de lignes, et vous pouvez tester chaque branche | Facile de se tromper subtilement : comparaison de chaînes, mauvais octets de clé ou corps analysé |
| Un chemin secret ou un jeton dans l'URL à la place | Simple | Fuit via les journaux, les proxys et les outils de suivi d'erreurs, ne prouve rien sur le corps et ne peut pas expirer. Ce n'est pas un substitut |

Ikiform signe chaque livraison, et la signature est le moyen pris en charge pour l'authentifier. Sa documentation ne publie pas de liste fixe d'adresses IP d'envoi, donc une liste d'IP autorisées n'est pas une vérification fiable.

## Vérifier dans votre framework

Pour les stacks JavaScript, le SDK fait le travail. Ce qui change d'un framework à l'autre, c'est l'emplacement de la route et la façon d'obtenir le corps avant que quoi que ce soit ne l'analyse. Choisissez le vôtre :

**Next.js**

Un route handler de l'App Router. `request.text()` est le corps brut ; appeler `request.json()` d'abord le perdrait. Le [guide sur votre propre serveur](https://ikiform.com/blog/form-submissions-to-your-own-server.md) ajoute la déduplication et `after()`.

`app/api/webhooks/ikiform/route.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

export async function POST(request: Request) {
  const payload = await request.text();
  try {
    const event = await verifyWebhook({
      payload,
      headers: request.headers,
      secret: process.env.IKIFORM_WEBHOOK_SECRET!,
    });
    console.info(event.type, request.headers.get("webhook-id"));
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
}
```

**Astro**

Un endpoint avec un export `POST`. Il nécessite le rendu à la demande, donc gardez `prerender = false` et un adaptateur. `getSecret` depuis `astro:env/server` lit `IKIFORM_WEBHOOK_SECRET` à l'exécution et uniquement sur le serveur ; `import.meta.env` l'intégrerait dans le build.

`src/pages/api/webhooks/ikiform.ts`:

```ts
import type { APIRoute } from "astro";
import { getSecret } from "astro:env/server";
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

export const prerender = false;

export const POST: APIRoute = async ({ request }) => {
  const payload = await request.text();
  try {
    const event = await verifyWebhook({
      payload,
      headers: request.headers,
      secret: getSecret("IKIFORM_WEBHOOK_SECRET") ?? "",
    });
    console.info(event.type, event.id);
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
};
```

**TanStack Start**

Une route serveur : la même route basée sur les fichiers, avec une fonction `server.handlers.POST` qui reçoit la `Request` standard.

`src/routes/api/webhooks/ikiform.ts`:

```ts
import { createFileRoute } from "@tanstack/react-router";
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

export const Route = createFileRoute("/api/webhooks/ikiform")({
  server: {
    handlers: {
      POST: async ({ request }) => {
        const payload = await request.text();
        try {
          const event = await verifyWebhook({
            payload,
            headers: request.headers,
            secret: process.env.IKIFORM_WEBHOOK_SECRET!,
          });
          console.info(event.type, event.id);
          return new Response(null, { status: 204 });
        } catch (error) {
          if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
          throw error;
        }
      },
    },
  },
});
```

**React Router**

Une resource route : un module avec une `action` et sans export par défaut, enregistré dans `routes.ts`. Elle répond directement à Ikiform au lieu de rendre une page.

`app/routes.ts`:

```ts
import { type RouteConfig, index, route } from "@react-router/dev/routes";

export default [
  index("routes/home.tsx"),
  route("webhooks/ikiform", "routes/webhooks.ikiform.ts"),
] satisfies RouteConfig;
```

`app/routes/webhooks.ikiform.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";
import type { Route } from "./+types/webhooks.ikiform";

export async function action({ request }: Route.ActionArgs) {
  if (request.method !== "POST") return new Response(null, { status: 405 });
  const payload = await request.text();
  try {
    const event = await verifyWebhook({
      payload,
      headers: request.headers,
      secret: process.env.IKIFORM_WEBHOOK_SECRET!,
    });
    console.info(event.type, event.id);
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
}
```

**SvelteKit**

Un endpoint `+server.ts`. La vérification CSRF intégrée de SvelteKit ne bloque que les types de contenu de formulaire cross-site, et Ikiform envoie `application/json`, donc aucune modification de config n'est nécessaire. Déclarez `IKIFORM_WEBHOOK_SECRET` dans `src/env.ts` avec `defineEnvVars` pour que `$app/env/private` puisse le lire.

`src/routes/webhooks/ikiform/+server.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";
import { IKIFORM_WEBHOOK_SECRET } from "$app/env/private";
import type { RequestHandler } from "./$types";

export const POST: RequestHandler = async ({ request }) => {
  const payload = await request.text();
  try {
    const event = await verifyWebhook({ payload, headers: request.headers, secret: IKIFORM_WEBHOOK_SECRET });
    console.info(event.type, event.id);
    return new Response(null, { status: 204 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) return new Response(error.reason, { status: 400 });
    throw error;
  }
};
```

**Nuxt**

Une route serveur Nitro. `readRawBody` renvoie le corps sous forme de chaîne avant que quoi que ce soit ne l'analyse. Définissez `NUXT_IKIFORM_WEBHOOK_SECRET` et déclarez `ikiformWebhookSecret` sous `runtimeConfig` dans `nuxt.config.ts`.

`server/api/webhooks/ikiform.post.ts`:

```ts
import { verifyWebhook, WebhookVerificationError } from "@ikiform/sdk/webhooks";

export default defineEventHandler(async (event) => {
  const payload = (await readRawBody(event, "utf8")) ?? "";
  const { ikiformWebhookSecret } = useRuntimeConfig(event);
  try {
    const delivery = await verifyWebhook({
      payload,
      headers: getRequestHeaders(event),
      secret: ikiformWebhookSecret,
    });
    console.info(delivery.type, delivery.id);
    setResponseStatus(event, 204);
    return null;
  } catch (error) {
    if (error instanceof WebhookVerificationError) {
      throw createError({ status: 400, message: error.reason });
    }
    throw error;
  }
});
```

Chaque gestionnaire répond `204` en cas de succès et `400` avec la raison de l'échec sinon. Faites le travail lent après avoir répondu ; Ikiform attend 15 secondes avant de compter une tentative comme échouée.

## Pourquoi le corps brut compte

La signature couvre des octets, pas un sens. `{"a":1}` et `{ "a": 1 }` sont le même JSON mais des octets différents, donc un corps analysé puis resérialisé ne correspond presque jamais. Il en va de même pour tout ce qui touche au corps à l'arrivée : un middleware qui analyse le JSON, un proxy qui réencode, ou un framework qui vous donne un objet au lieu d'une chaîne.

Lisez le corps une seule fois, en octets ou en chaîne, vérifiez-le, puis analysez cette même chaîne. Chaque guide par langage montre où se trouve le corps brut dans ses frameworks, et le [guide sur votre propre serveur](https://ikiform.com/blog/form-submissions-to-your-own-server.md) contient un tableau pour les frameworks Node et Python courants.

## Comparer en temps constant

Une comparaison de chaînes normale s'arrête au premier caractère différent. En théorie, un attaquant capable de chronométrer des milliers de requêtes apprend combien de caractères initiaux d'une tentative sont justes et construit une signature valide caractère par caractère. Une comparaison en temps constant prend le même temps quelle que soit la part qui correspond. Chaque langage en a une :

| Langage | Comparaison en temps constant |
| --- | --- |
| Node.js | `crypto.timingSafeEqual(a, b)`, qui lève une exception si les longueurs diffèrent, donc vérifiez d'abord la longueur |
| Python | `hmac.compare_digest(a, b)` |
| Go | `hmac.Equal(a, b)` |
| PHP | `hash_equals($known, $user)` |
| Ruby | `OpenSSL.secure_compare(a, b)`, ou `ActiveSupport::SecurityUtils.secure_compare` dans Rails |

## Timestamps et rejeux

Une requête valide capturée une fois reste valide pour toujours, à moins que quelque chose ne la fasse expirer. Le timestamp signé joue ce rôle : rejetez toute livraison à plus de 5 minutes de votre horloge, dans un sens ou dans l'autre. Comme Ikiform signe à nouveau chaque tentative avec un timestamp récent, une nouvelle tentative trois jours plus tard passe encore ; seule une ancienne requête copiée échoue.

Dans ces 5 minutes, un rejeu reste possible, tout comme un véritable doublon : Ikiform réessaie quand il ne reçoit pas de `2xx` à temps, même si votre gestionnaire a terminé. Stockez `webhook-id` avec une contrainte d'unicité et ignorez les identifiants déjà traités. Cela règle les deux cas.

> Gardez l'horloge du serveur synchronisée. Une horloge qui dérive de plus de 5 minutes rejette chaque livraison avec `timestamp_too_old` ou `timestamp_too_new`. Les plateformes managées synchronisent généralement les horloges ; sur votre propre VM, exécutez NTP.

## Faire tourner le secret

Faites tourner le secret quand quelqu'un qui le connaissait s'en va, quand il a pu être journalisé ou commité, ou selon un calendrier. **Roll signing secret** sur la page de l'endpoint en crée un nouveau, et pendant 24 heures chaque livraison est signée avec les deux :

Schéma : Ce que contient webhook-signature avant, pendant et après une rotation de secret

1. Avant la rotation, chaque livraison porte une seule signature, faite avec l'ancien secret.
2. Vous choisissez Roll signing secret et copiez le nouveau secret.
3. Pendant les 24 heures suivantes, chaque livraison porte deux signatures, donc un serveur qui détient l'un ou l'autre secret l'accepte. Déployez le nouveau secret pendant cette fenêtre.
4. Après 24 heures, les livraisons ne portent plus que la nouvelle signature, et un serveur qui détient encore l'ancien secret les rejette.

Votre gestionnaire n'a besoin d'aucun code particulier pour cela, tant qu'il vérifie chaque entrée v1 au lieu de la seule première.

Si l'ancien secret est toujours déployé après 24 heures, chaque livraison échoue. Ikiform réessaie chacune 8 fois sur environ trois jours, donc une fois le bon secret déployé, utilisez **Retry** dans le journal de livraison pour tout ce qui a déjà abandonné.

## Après la vérification

Une signature valide prouve que la requête vient d'Ikiform et n'a pas été modifiée. Elle ne prouve pas que le contenu est sûr ou nouveau. Trois habitudes complètent le travail :

- **Ignorez les répétitions.** Stockez `webhook-id`, ou l'`id` du payload, qui a la même valeur, avec une contrainte d'unicité. Une nouvelle tentative d'une livraison déjà traitée devient sans effet.
- **Répondez d'abord, travaillez ensuite.** Renvoyez `2xx` dès que l'événement est stocké ou mis en file d'attente. Ikiform compte comme un échec tout ce qui dépasse 15 secondes et réessaie, ce qui ramène le problème des doublons.
- **Considérez les champs comme non fiables.** `data.fields` contient ce qu'un visiteur a tapé dans un formulaire public. Échappez-le avant de l'afficher en HTML, dans un message de chat ou dans un e-mail, et ne le passez jamais à un shell ou à une requête non paramétrée.

### Tester avant la mise en production

Ikiform ne livre qu'à des URL `https:` publiques sur le port 443, donc `localhost` ne peut pas recevoir de webhook. Lancez un tunnel comme `cloudflared tunnel --url http://localhost:3000` et ajoutez l'URL qu'il affiche comme second endpoint pendant que vous travaillez. **Send test** sur la page de l'endpoint envoie une fois un événement `endpoint.test`, sans nouvelles tentatives, signé exactement comme une vraie livraison. Supprimez l'endpoint de test quand vous avez terminé ; il a son propre secret, donc rien d'autre ne change.

Si une livraison échoue et que vous ne voyez pas pourquoi, copiez ses en-têtes et son corps depuis vos journaux dans le [vérificateur de signature](https://ikiform.com/fr/docs/webhooks.md#verifier). Il exécute la même vérification dans le navigateur et vous indique quelle étape a échoué.

## Cinq façons de rater la vérification

| Erreur | Symptôme | Solution |
| --- | --- | --- |
| Utiliser toute la chaîne `whsec_...` comme clé | `no_matching_signature` à chaque livraison | Retirez `whsec_`, puis décodez en base64. La clé fait 32 octets |
| Vérifier un corps analysé | `no_matching_signature`, parfois seulement pour certains payloads | Vérifiez le corps brut ; analysez ensuite |
| Ne vérifier que la première signature | Des échecs qui commencent juste après une rotation | Parcourez chaque entrée `v1` séparée par des espaces |
| Hexadécimal au lieu de base64 | `no_matching_signature` | Standard Webhooks utilise le base64 standard avec padding, pas l'hexadécimal ni le base64url |
| Chercher les en-têtes avec la casse exacte | `missing_headers` derrière certains proxys | Les noms d'en-têtes ne sont pas sensibles à la casse. Ikiform les envoie en minuscules ; recherchez-les sans tenir compte de la casse |

Il n'y a pas d'intégrations en un clic. Chaque outil est atteint via un [webhook](https://ikiform.com/fr/docs/webhooks.md) signé, directement ou via un outil d'automatisation comme [Zapier](https://ikiform.com/blog/form-submissions-to-zapier.md) ou [n8n](https://ikiform.com/blog/form-submissions-to-n8n.md). Un espace de travail peut avoir 3 endpoints, chacun avec son propre secret. Le payload complet, le calendrier des nouvelles tentatives et le journal de livraison se trouvent dans [Webhooks](https://ikiform.com/fr/docs/webhooks.md).

## Choisissez votre cas

Chaque page par langage contient des gestionnaires idiomatiques, une option avec bibliothèque et un test unitaire qui exécute le vecteur de test de cette page.

- [Node.js](https://ikiform.com/fr/guides/webhook-signature-verification/nodejs.md): Vérifiez les signatures des webhooks Ikiform en Node.js : corps bruts dans Express et Fastify, le SDK, le package standardwebhooks et une version node:crypto testée.
- [Python](https://ikiform.com/fr/guides/webhook-signature-verification/python.md): Vérifiez les signatures des webhooks Ikiform dans Flask, FastAPI et Django, avec le package standardwebhooks ou la bibliothèque standard, plus un test pytest.
- [Go net/http](https://ikiform.com/fr/guides/webhook-signature-verification/go.md): Vérifiez les signatures des webhooks Ikiform en Go avec net/http : lisez le corps une seule fois, vérifiez-le avec crypto/hmac, testez-le, ou utilisez la bibliothèque Standard Webhooks.
- [PHP](https://ikiform.com/fr/guides/webhook-signature-verification/php.md): Vérifiez les signatures des webhooks Ikiform en PHP simple avec hash_hmac et hash_equals, et dans Laravel avec la bibliothèque Standard Webhooks et une exception CSRF.
- [Ruby](https://ikiform.com/fr/guides/webhook-signature-verification/ruby.md): Vérifiez les signatures des webhooks Ikiform dans Sinatra avec OpenSSL et un test Minitest, et dans Rails avec la gem standardwebhooks et request.raw_post.

## Vérifiez que tout fonctionne

Faites-le d'abord sur un endpoint de staging, puis à nouveau en production. Ikiform n'a pas d'offre gratuite. Les tests sont gratuits : un espace de travail sans offre accepte 50 soumissions de test par mois depuis `localhost`. Pour passer en production, choisissez Pro à partir de 6 $/mois pour 500 soumissions, ou payez une seule fois : 10 000 soumissions pour 49 $ ou 50 000 soumissions pour 149 $, avec des crédits qui n'expirent jamais. Voir les [tarifs](https://ikiform.com/fr/pricing).

1. Passez le vecteur de test dans votre code : il doit produire la signature attendue, accepter l'en-tête à deux signatures et rejeter le corps mis en forme.
2. Dans le tableau de bord, ouvrez **Webhooks**, ajoutez l'URL `https:` publique de votre endpoint et copiez le secret `whsec_` dans l'environnement de votre serveur.
3. Choisissez **Send test** sur la page de l'endpoint. Le journal de livraison affiche votre `204` et l'événement arrive dans vos journaux.
4. Modifiez un caractère du secret sur votre serveur et envoyez un autre test. Le journal affiche `400` avec `no_matching_signature`.
5. Envoyez le formulaire pour de vrai et vérifiez que la livraison `submission.created` réussit.

## Dépannage

| Problème | Que faire |
| --- | --- |
| `400 no_matching_signature` | Mauvais secret, corps analysé, ou clé non décodée en base64. Passez le vecteur de test par le même chemin de code pour savoir lequel. |
| `400 timestamp_too_old` ou `timestamp_too_new` | L'horloge du serveur est décalée, ou vous rejouez à la main une requête enregistrée. Synchronisez l'horloge, et utilisez **Retry** dans le journal de livraison, qui signe à nouveau. |
| `400 missing_headers` | Un proxy ou un framework a supprimé les en-têtes `webhook-*`, ou la requête ne vient pas d'Ikiform. Journalisez chaque nom d'en-tête que voit votre gestionnaire. |
| Fonctionne pour les événements de test, échoue pour les vraies soumissions | Les vrais payloads contiennent des caractères non ASCII et des textes plus longs. Votre code décode ou réencode le corps avant de le vérifier. Vérifiez les octets exacts. |
| Chaque livraison échoue juste après la rotation du secret | Le gestionnaire ne vérifie que la première signature, ou les 24 heures se sont écoulées avec l'ancien secret déployé. Vérifiez chaque entrée `v1` et déployez le nouveau secret. |
| Le journal de livraison affiche un délai dépassé, mais votre gestionnaire s'est exécuté | Ikiform attend 15 secondes. Répondez d'abord et faites le travail lent ensuite, et ignorez les valeurs `webhook-id` répétées. |

## Questions

### Comment vérifier une signature de webhook ?

Recalculez le HMAC-SHA256 de l'identifiant du webhook, du timestamp et du corps brut joints par des points, avec comme clé la partie décodée en base64 de votre secret whsec_. Encodez le résultat en base64 et comparez-le en temps constant avec chaque valeur v1 de l'en-tête webhook-signature.

### Pourquoi ma signature de webhook ne correspond-elle jamais ?

Presque toujours pour l'une de ces trois raisons : le corps a été analysé et resérialisé avant la vérification, toute la chaîne whsec_ a servi de clé au lieu de ses octets décodés en base64, ou le secret appartient à un autre endpoint.

### Qu'est-ce que Standard Webhooks ?

Une spécification ouverte pour signer les webhooks avec HMAC-SHA256 et les en-têtes webhook-id, webhook-timestamp et webhook-signature. Ikiform la suit, donc n'importe quelle bibliothèque Standard Webhooks peut vérifier les livraisons d'Ikiform.

### Pourquoi rejeter les anciens timestamps ?

Sinon, une requête capturée resterait valide pour toujours. Rejeter tout ce qui est à plus de 5 minutes de votre horloge empêche les rejeux d'anciennes requêtes, et Ikiform signe chaque nouvelle tentative avec un timestamp récent, donc les vraies nouvelles tentatives passent toujours.

### Qu'arrive-t-il aux livraisons quand je fais tourner le secret ?

Pendant 24 heures, chaque livraison est signée avec l'ancien et le nouveau secret, donc vous pouvez déployer le nouveau sans perdre d'événements. Ensuite, seul le nouveau secret fonctionne.

### Mon gestionnaire doit-il renvoyer 401 ou 400 pour une signature invalide ?

Les deux conviennent pour Ikiform ; tout statut autre que 2xx compte comme un échec et donne lieu à une nouvelle tentative. Ne renvoyez pas 410, qui désactive immédiatement l'endpoint.

## Autres guides

- [Node.js](https://ikiform.com/fr/guides/webhook-signature-verification/nodejs.md): Vérifiez les signatures des webhooks Ikiform en Node.js : corps bruts dans Express et Fastify, le SDK, le package standardwebhooks et une version node:crypto testée.
- [Python](https://ikiform.com/fr/guides/webhook-signature-verification/python.md): Vérifiez les signatures des webhooks Ikiform dans Flask, FastAPI et Django, avec le package standardwebhooks ou la bibliothèque standard, plus un test pytest.
- [Go net/http](https://ikiform.com/fr/guides/webhook-signature-verification/go.md): Vérifiez les signatures des webhooks Ikiform en Go avec net/http : lisez le corps une seule fois, vérifiez-le avec crypto/hmac, testez-le, ou utilisez la bibliothèque Standard Webhooks.
- [PHP](https://ikiform.com/fr/guides/webhook-signature-verification/php.md): Vérifiez les signatures des webhooks Ikiform en PHP simple avec hash_hmac et hash_equals, et dans Laravel avec la bibliothèque Standard Webhooks et une exception CSRF.
- [Ruby](https://ikiform.com/fr/guides/webhook-signature-verification/ruby.md): Vérifiez les signatures des webhooks Ikiform dans Sinatra avec OpenSSL et un test Minitest, et dans Rails avec la gem standardwebhooks et request.raw_post.
- [Liste d'attente pour un nouveau SaaS](https://ikiform.com/fr/guides/waitlist.md): Créez une liste d'attente avant lancement sur votre landing page : le formulaire, l'anti-spam, les doublons, les e-mails de confirmation et un export propre le jour J.
- [Formulaire de contact sur un site statique](https://ikiform.com/fr/guides/contact-form-static-site.md): Ajoutez un formulaire de contact fonctionnel à un site statique sans serveur : où va le POST, le spam, les réponses par e-mail, et du code pour chaque hébergeur.
- [Recevoir un formulaire par e-mail](https://ikiform.com/fr/guides/send-form-to-email.md): Recevez chaque soumission dans votre boîte sans serveur mail : pourquoi mailto : et mail() de PHP échouent, et une solution qui filtre le spam et permet de répondre.
