Documentation › Webhooks
Webhooks
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é.
État
Les webhooks existent sur le serveur, pour les intégrations machine. Aujourd’hui, l’adresse de réception se règle par un administrateur de Subagent, pour une marque entière. Aucun outil ne permet encore à un espace de déclarer la sienne : c’est [ à venir ].
D’ici là, un agent n’a pas besoin de webhook : il lit ses réponses avec list_threads (status = replied), puis get_thread. Voir Fils.
Les événements
| Événement | Quand | Données (data) |
|---|---|---|
message.sent | Un message est parti. | message_id, thread_id, inbox_id, to, subject, metadata, dry_run |
message.delivered | Le serveur du destinataire a confirmé la remise. | message_id, thread_id, inbox_id, to, subject, metadata |
message.bounced | Un message est revenu : adresse en échec. | Les mêmes, et bounce : type, status, diagnostic |
message.rejected | Une règle d’envoi a refusé un message au moment de partir. | message_id, thread_id, inbox_id, to, code, reason |
message.unsubscribed | Une personne s’est désinscrite, ou a signalé un message. | address, via, reason, message_id |
thread.replied | Le correspondant a répondu dans un fil. | thread_id, message_id, inbox_id, from, subject, preview, auto_reply |
thread.received | Un nouvel e-mail a ouvert un fil. | Les mêmes que thread.replied |
inbox.suspended | Le coupe-circuit a suspendu une boîte. | inbox_id, address, reason |
Une réponse automatique ne déclenche pas thread.replied.
La forme d’un appel
{
"id": "whd_9b2e4c7a1d5f48e3a6c0b8d2f4e6a1c3",
"type": "thread.replied",
"created_at": "2026-10-09T16:12:00+02:00",
"tenant_id": "ten_8d41c0b7a2e94f6cb3159e07d2a8c6f1",
"data": {
"thread_id": "thr_b81f4c6a2d9e47035c1a8e6f0b4d2c97",
"message_id": "msg_e93b7d1a5c0f42b8d6e2a9c4f7b1d056",
"inbox_id": "inb_3f9c2a7e5b1d4c8e9a0f6b2d7c4e1a95",
"from": "nadia.morel@exemple.com",
"subject": "Re: Rendez-vous de jeudi",
"preview": "Bonjour, vendredi à la même heure me convient.",
"auto_reply": false
}
}| En-tête | Contenu |
|---|---|
Subagent-Signature | t=<horodatage>,v1=<signature> |
Subagent-Event | Le type de l’événement, par exemple thread.replied. |
Subagent-Delivery | L’identifiant de la remise, whd_…. Le même à chaque réessai. |
Content-Type | application/json |
Vérifier la signature
La signature v1 est le HMAC-SHA256, en hexadécimal, de la chaîne <horodatage>.<corps brut>, calculé avec le secret du webhook (whsec_…, affiché une seule fois à sa création).
- Lisez
tetv1dans l’en-têteSubagent-Signature. - Refusez un horodatage trop ancien : cinq minutes est une tolérance courante.
- Calculez le HMAC-SHA256 de
t, un point, puis le corps brut de la requête, avec votre secret. - Comparez le résultat à
v1, en temps constant.
import hashlib, hmac, time
def signature_valide(secret: str, entete: str, corps: bytes, tolerance: int = 300) -> bool:
champs = dict(p.strip().split("=", 1) for p in entete.split(","))
horodatage = int(champs["t"])
if abs(time.time() - horodatage) > tolerance:
return False
attendu = hmac.new(secret.encode(), f"{horodatage}.".encode() + corps, hashlib.sha256).hexdigest()
return hmac.compare_digest(attendu, champs["v1"])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.
- Un même événement peut donc arriver deux fois : reconnaissez-le à son
id.