Aller au contenu
Maildocs
Sommaire de la documentation

Documentation › Webhooks

Webhooks

Voir en .md

Le serveur signale par webhook ce qui arrive à un message ou à un fil : un départ, une remise, un rebond, une réponse, une désinscription. Chaque appel est un POST JSON signé au format Standard Webhooks. Les mêmes événements se relisent par curseur avec event_feed.

État

Un espace déclare l’adresse qui reçoit ses événements avec set_webhook, et la retire avec remove_webhook. Ce sont des gestes de l’humain de l’espace, depuis son assistant : permission approve. get_settings dit si un webhook est déclaré.

Un agent n’a pas besoin de webhook : il relève son courrier avec list_threads (unanswered = true), puis get_thread. Voir Recevoir du courrier. Un programme qui veut les événements sans exposer d’adresse les relit avec event_feed, une clé read suffit : voir plus bas.

Les événements

ÉvénementQuandDonnées (data)
mail.message.sentUn message est parti.message_id
mail.message.deliveredLe serveur du destinataire a confirmé la remise.message_id
mail.message.bouncedUn message est revenu, ou le serveur distant l’a refusé.message_id, permanent (true : adresse morte ou refus définitif ; false : refus de politique, l’adresse n’est pas tenue pour morte), code (le code étendu, 5.0.0 si l’avis n’en donne pas), diagnostic (200 caractères au plus)
mail.message.rejectedUne règle d’envoi a refusé un message au moment de partir.message_id, code, reason
mail.message.unsubscribedUne personne s’est désinscrite, ou a signalé un message comme indésirable.message_id (ou null), origine : lien (le lien de désinscription), stop (un « STOP » dans une réponse) ou plainte
mail.person.opposedUne adresse a été bloquée à la main, avec add_suppression.origine : manuel
mail.thread.repliedLe correspondant a répondu dans un fil.message_id, extrait (le début du texte, 200 caractères au plus, sur une ligne), automatique (true pour une réponse automatique)
mail.message.receivedUn nouvel e-mail a ouvert un fil.Les mêmes que mail.thread.replied
mail.inbox.suspendedLe coupe-circuit a suspendu une boîte.address, reason
mail.webhook.testUn webhook vient d’être déclaré : c’est un essai, pour son seul webhook.message

Un type que vous ne connaissez pas s’ignore : la liste peut s’allonger. Le blocage d’un domaine entier ne nomme personne : il n’émet rien.

La forme d’un appel

Le corps est l’enveloppe de l’événement, commune à la famille Escouade : son identifiant (evt_…), son type, sa date en UTC, l’espace (account), ce dont il parle (subject), ses données (data) et la version de l’enveloppe (v).

Corps de la requête
{
  "id": "evt_9b2e4c7a1d5f48e3a6c0b8d2f4e6a1c3",
  "type": "mail.thread.replied",
  "occurred_at": "2026-10-09T14:12:00Z",
  "account": "ten_8d41c0b7a2e94f6cb3159e07d2a8c6f1",
  "subject": {
    "person": { "email": "[email protected]" },
    "refs": {
      "mail.inbox": "inb_3f9c2a7e5b1d4c8e9a0f6b2d7c4e1a95",
      "mail.message": "msg_e93b7d1a5c0f42b8d6e2a9c4f7b1d056",
      "mail.thread": "thr_b81f4c6a2d9e47035c1a8e6f0b4d2c97"
    }
  },
  "data": {
    "message_id": "msg_e93b7d1a5c0f42b8d6e2a9c4f7b1d056",
    "extrait": "Bonjour, vendredi à la même heure me convient.",
    "automatique": false
  },
  "v": 1
}
  • subject.person.email : le correspondant (le destinataire d’un envoi, l’expéditeur d’une réponse, la personne désinscrite ou bloquée), en minuscules.
  • subject.refs : les objets concernés, mail.message, mail.thread et mail.inbox ; mail.campaign (cmp_…) quand le message part d’une campagne ou y répond ; les références de Prescriptio (prescriptio.organisation, prescriptio.campagne, prescriptio.contact, prescriptio.envoi) quand le fil a été ouvert par Prescriptio. Une référence inconnue s’ignore.
En-têteContenu
webhook-idL’identifiant de l’événement, evt_…. Le même à chaque réessai, et dans event_feed.
webhook-timestampL’heure de l’envoi, en secondes Unix.
webhook-signaturev1,<signature>
Content-Typeapplication/json

Vérifier la signature

La signature v1 est le HMAC-SHA256, en base64, de la chaîne <webhook-id>.<webhook-timestamp>.<corps brut>. Sa clé est le secret du webhook (whsec_…, affiché une seule fois à sa déclaration) sans son préfixe whsec_, décodé du base64. C’est le format Standard Webhooks : ses bibliothèques le vérifient telles quelles.

  1. Lisez les en-têtes webhook-id, webhook-timestamp et webhook-signature.
  2. Refusez un horodatage trop ancien : cinq minutes est une tolérance courante.
  3. Calculez le HMAC-SHA256 de l’identifiant, un point, l’horodatage, un point, puis le corps brut de la requête, avec la clé du secret.
  4. Comparez v1, suivi du résultat en base64 à chaque signature de l’en-tête (séparées par une espace), en temps constant.
Python
import base64, hashlib, hmac, time

def signature_valide(secret: str, entetes: dict, corps: bytes, tolerance: int = 300) -> bool:
    identifiant = entetes["webhook-id"]
    horodatage = entetes["webhook-timestamp"]
    if abs(time.time() - int(horodatage)) > tolerance:
        return False
    cle = base64.b64decode(secret.removeprefix("whsec_"))
    calcul = hmac.new(cle, f"{identifiant}.{horodatage}.".encode() + corps, hashlib.sha256).digest()
    attendu = "v1," + base64.b64encode(calcul).decode()
    return any(hmac.compare_digest(attendu, s) for s in entetes["webhook-signature"].split())

Réessais

  • Une remise réussit quand votre serveur répond par un code 2xx, en moins de 10 secondes.
  • Sinon elle est réessayée, avec un délai qui s’allonge : 30 secondes, 2 minutes, 8 minutes, 32 minutes, un peu plus de 2 heures, puis 6 heures.
  • Après 8 tentatives, la remise est abandonnée. L’événement reste lisible avec event_feed.
  • Un même événement peut donc arriver deux fois, et aussi par event_feed : reconnaissez-le à son id, et traitez-le une seule fois.
  • Une remise close, réussie ou abandonnée, est effacée après 30 jours.

set_webhook

Déclare l’adresse qui reçoit les événements de l’espace. Permission : approve. L’espace doit être vérifié.

ParamètreTypeObligatoireDescription
urltexteouiUne adresse publique en https, 500 caractères au plus, sans identifiant ni mot de passe.
  • Le secret de signature (whsec_…) est rendu une seule fois, dans la réponse (secret). Gardez-le comme une clé.
  • Un événement d’essai, mail.webhook.test, part dans la minute : s’il n’arrive pas, vérifiez l’adresse.
  • Une adresse déjà déclarée est remplacée, avec un secret neuf ; ses remises en attente sont abandonnées. Une déclaration par minute au plus (code webhook_too_soon), et dix en 24 heures (code space_rate_limited).
  • Une adresse privée, de bouclage ou de lien local est refusée, et les redirections ne sont pas suivies.
  • L’humain de l’espace reçoit un e-mail à chaque webhook déclaré ou retiré. Il ne contient pas le secret.
  • Une adresse qui ne répond plus finit retirée : après 50 échecs de suite, le serveur retire le webhook et prévient l’humain par e-mail. Les événements non remis ne sont pas renvoyés ; ils restent lisibles avec event_feed.
  • Un secret d’avant le format Standard Webhooks ne signe plus : ses remises échouent. Redéclarez l’adresse avec set_webhook pour recevoir un secret neuf.
  • L’adresse ne porte pas de clé (code secret_in_message).
  • Un espace a un seul webhook : il reçoit tous les événements de l’espace.

remove_webhook

Retire le webhook de l’espace : son adresse ne reçoit plus rien, et les remises en attente sont abandonnées. Permission : approve. Sans paramètre.

event_feed

Relit par curseur les événements de l’espace, dans l’ordre où ils ont été émis : les mêmes que ses webhooks, avec la même enveloppe et le même id. Permission : read. L’espace peut être vérifié ou non, et aucun webhook n’a besoin d’être déclaré.

ParamètreTypeObligatoireDescription
aftertextenonLe curseur next_cursor de l’appel précédent, tel quel. Absent : le flux part du plus ancien événement gardé.
limitentiernonNombre d’événements rendus, de 1 à 500 (100 par défaut).
  • La réponse porte events, les enveloppes dans l’ordre d’émission, et next_cursor, toujours rendu : le même que after quand rien de neuf n’est arrivé. Repassez-le en after à l’appel suivant.
  • Les événements sont gardés trente-cinq jours ; au-delà, ils quittent le flux.
  • Une clé bornée à des boîtes ne voit que les événements de ses boîtes : ni ceux d’une autre boîte, ni ceux de l’espace qui ne nomment aucune boîte, comme mail.person.opposed ou mail.webhook.test.
  • Un événement peut paraître dans le flux un peu après son webhook : le flux attend que les écritures plus anciennes soient closes, pour n’en laisser aucune derrière un curseur déjà rendu. Un curseur que le serveur ne reconnaît plus (après une restauration de sa base) fait repartir le flux du début : des doublons, jamais de perte.
  • Traitez chaque id une seule fois : un événement relu par le flux a pu arriver déjà par webhook.
  • Un curseur illisible est refusé avec le code invalid_argument.
Cette page en Markdown : /mail/docs/webhooks.md Toute la documentation pour un agent : /mail/docs/llms.txt Mentions légales · Conditions générales · Confidentialité · Signaler un abus