# Webhooks

Mail envoie ses webhooks au format [Standard Webhooks](https://www.standardwebhooks.com/), le même que Svix et Resend : une bibliothèque de ce standard, dans le langage de ton application, vérifie la signature à ta place. Pour créer une adresse, voir [Recevoir les réponses d’un formulaire par webhook](/docs/guides/recevoir-les-reponses-par-webhook/).

## Événements

| Événement               | Quand                           |
| ----------------------- | ------------------------------- |
| `form.response.created` | Quelqu’un envoie un formulaire. |

## Le message

Un `POST` en JSON, 256 Ko au plus.

```json
{
  "type": "form.response.created",
  "timestamp": "2026-10-17T09:12:03.000Z",
  "data": {
    "id": "k57d…",
    "form_id": "j97a…",
    "form_public_id": "k7m2p9x4q8ab",
    "form_name": "Contact",
    "values": { "prenom": "Léa", "email": "lea@studio.ch", "message": "Un devis, s'il vous plaît." },
    "email": "lea@studio.ch",
    "consent": { "status": "pending", "text": "J'accepte de recevoir vos nouvelles", "at": "2026-10-17T09:12:03.000Z" },
    "page": "https://monsite.ch/contact",
    "created_at": "2026-10-17T09:12:03.000Z"
  }
}
```

| Champ          | Sens                                                                                                                                         |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `data.id`      | Identifiant de la réponse. Le même que dans [`GET /forms/:id/responses`](/docs/reference/api-formulaires/).                                  |
| `data.values`  | Une valeur par champ, avec la variable du champ comme clé. Une case cochée vaut `oui`.                                                       |
| `data.email`   | L’adresse du champ e-mail, ou `null`.                                                                                                        |
| `data.consent` | La preuve de consentement : `status` vaut `pending` (en attente du lien de confirmation) ou `confirmed`. `null` si la case n’est pas cochée. |
| `data.page`    | La page du site d’où vient la réponse, sans paramètres.                                                                                      |

## En-têtes

| En-tête             | Contenu                                                                       |
| ------------------- | ----------------------------------------------------------------------------- |
| `webhook-id`        | Identifiant du message, `msg_…`. Identique à chaque essai et à chaque renvoi. |
| `webhook-timestamp` | Heure d’envoi, en secondes. Renouvelée à chaque essai.                        |
| `webhook-signature` | `v1,` suivi de la signature en base64.                                        |

## Vérifier la signature

1. Refuse le message si `webhook-timestamp` s’écarte de plus de 5 minutes de l’heure de ton serveur. C’est ce qui empêche de rejouer un message volé.
2. Prends le secret `whsec_…`, enlève le préfixe `whsec_` et décode le reste en base64 : c’est la clé.
3. Calcule le HMAC-SHA256 de `webhook-id`, un point, `webhook-timestamp`, un point, puis le corps **brut** de la requête (avant toute lecture du JSON).
4. Encode le résultat en base64 et compare-le, en temps constant, à chaque signature de `webhook-signature` (séparées par une espace, chacune préfixée `v1,`).

```ts
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64")
const signed = `${id}.${timestamp}.${rawBody}`
const expected = crypto.createHmac("sha256", key).update(signed).digest()
const ok = signatureHeader.split(" ").some((part) => {
  const [version, value] = part.split(",")
  const received = Buffer.from(value ?? "", "base64")
  return version === "v1" && received.length === expected.length && crypto.timingSafeEqual(received, expected)
})
```

Réponds `401` si la signature est fausse. Réponds `2xx` quand le message est traité ou déjà vu.

## Livraison

|             |                                                                                                                     |
| ----------- | ------------------------------------------------------------------------------------------------------------------- |
| Succès      | Une réponse 2xx en 10 secondes.                                                                                     |
| Redirection | Un échec : Mail ne suit jamais une redirection. Donne l’adresse finale.                                             |
| Essais      | 8 au total : tout de suite, puis après 5 secondes, 5 minutes, 30 minutes, 2 heures, 5 heures, 10 heures, 10 heures. |
| Renvoi      | Depuis la page Webhooks, bouton **Livraisons** puis **Renvoyer**. Même `webhook-id`.                                |
| Journal     | 30 jours.                                                                                                           |

## Adresses acceptées

https uniquement, port 443, un nom de domaine public. Sont refusés : `http`, un numéro IP, `localhost` et les noms internes (`.local`, `.internal`…), un nom qui pointe vers une adresse privée, une adresse avec identifiant et mot de passe. Chaque espace garde 20 adresses au plus.
