Aller au contenu

Chaque changement sur vos numéros, signé et envoyé chez vous.

Dix événements couvrent les messages, les statuts, les réactions, les discussions, les groupes et les numéros eux-mêmes. Chacun arrive en POST JSON signé, relancé pendant 24 heures jusqu'à ce que votre endpoint réponde.

POST https://example.com/hooks/relay
Content-Type: application/json
User-Agent: relay-webhooks/1
X-Relay-Event: message.status
X-Relay-Delivery: dlv_01a1cf088c4e7d2a9b5e1c6f3a8d0e72
X-Relay-Timestamp: 1794474850
X-Relay-Signature: v1=…
{
"id": "evt_01a1cf088c4e7d2a9b5e1c6f3a8d0e72",
"event": "message.status",
"created_at": "2026-11-12T09:14:10.502Z",
"data": { "id": "msg_…", "status": "read", … }
}
Signature
HMAC-SHA256
Répondre sous
10 secondes
Relances
24 heures, jusqu'à 10 tentatives
Livraison
Au moins une fois

Dix événements

Filtrez chaque endpoint par événement et par numéro. Un filtre vide reçoit tout, y compris les événements et les numéros ajoutés plus tard.

Messages

Discussions et groupes

Numéros

message.received

Un message est arrivé sur l'un de vos numéros, en discussion individuelle ou en groupe.

data est l'objet Message complet, le même que renvoie GET /v1/messages/{id}.

X-Relay-Event:message.received
{
"id": "evt_01a1cf082f8a7c51b6e0d4a39f12c87e",
"event": "message.received",
"created_at": "2026-11-12T09:13:02.410Z",
"data": {
"id": "msg_01a1cf082d307e19a4c3b85f20d6e91a",
"number_id": "num_01a1a1c6c2007a3f9c41e2b7d5a60813",
"chat_id": "chat_01a1a1d0f4a87c128e5d4b9a2f61c37e",
"chat_type": "dm",
"chat_jid": "212612345678@s.whatsapp.net",
"wa_message_id": "3A9F0C21B7E4D85A6C12",
"from_me": false,
"sender": {
"jid": "212612345678@s.whatsapp.net",
"phone": "+212612345678",
"name": "Maison Exemple",
"contact_id": "ct_01a1a1d0f4b07e2c9a143c8f5b2e7d90"
},
"type": "text",
"body": "Perfect, thank you!",
"content": null,
"media": null,
"quoted_message_id": null,
"quoted_wa_message_id": null,
"mentioned_jids": [],
"is_forwarded": false,
"is_view_once": false,
"status": "delivered",
"error_code": null,
"source": "live",
"client_key": null,
"sent_at": "2026-11-12T09:13:02.000Z",
"edited_at": null,
"revoked_at": null
}
}
  • webhook.test n'est envoyé que par POST /v1/webhooks/endpoints/{id}/test ou par « Envoyer un test » dans le tableau de bord, jamais par un vrai changement.
  • Les événements décrivent l'activité en direct. Les messages importés de l'historique d'un numéro lors de sa liaison, ou récupérés après une coupure, se lisent via l'API mais ne sont pas envoyés en événements.

Vérifiez chaque requête

Vérifiez la signature sur le corps brut avant de l'analyser, et rejetez tout ce qui date de plus de cinq minutes.

En-têtes

En-têteCe qu'il porteExemple
Content-TypeToujours du JSON.application/json
User-AgentIdentifie notre émetteur.relay-webhooks/1
X-Relay-EventLe nom de l'événement, identique à event dans le corps.message.received
X-Relay-DeliveryL'identifiant de livraison, tel qu'il apparaît dans le journal des livraisons.dlv_01a1cf082f8a…
X-Relay-TimestampSecondes Unix au moment où cette tentative a été signée.1794474782
X-Relay-Signaturev1= suivi du HMAC-SHA256 en hexadécimal.v1=5f2b…

Le schéma v1

signed_payload = X_Relay_Timestamp + "." + raw_body
signature = "v1=" + hex(HMAC_SHA256(secret, signed_payload))
  1. Lisez le corps brut en octets, avant toute analyse JSON.
  2. Concaténez X-Relay-Timestamp, un point et le corps brut.
  3. Calculez le HMAC-SHA256 avec le secret de l'endpoint (toute la chaîne whsec_…), encodez-le en hexadécimal et préfixez v1=.
  4. Comparez le résultat avec X-Relay-Signature en temps constant.
  5. Rejetez un horodatage à plus de 300 secondes (5 minutes) de votre horloge.

Le secret (whsec_…) s'affiche à l'ajout de l'endpoint et à chaque rotation. Une rotation invalide l'ancien secret immédiatement : mettez à jour votre récepteur juste après.

POST/hooks/relay
import crypto from "node:crypto";
import express from "express";
const app = express();
const secret = process.env.RELAY_WEBHOOK_SECRET; // whsec_…
app.post("/hooks/relay", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.get("X-Relay-Timestamp") ?? "";
const given = req.get("X-Relay-Signature") ?? "";
const expected = "v1=" + crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${req.body}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
const valid = given.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
if (!fresh || !valid) return res.sendStatus(401);
const event = JSON.parse(req.body);
res.sendStatus(204);
// Skip event.id if you have handled it, then act on event.event and event.data.
});
app.listen(3000);

Livraison

Au moins une fois, signée à chaque tentative, relancée avec un délai croissant pendant 24 heures.

Répondez par un 2xx en moins de 10 secondes, puis faites le travail. Les délais dépassés, les erreurs réseau, 408, 429 et 5xx sont relancés ; tout autre statut est définitif, et les redirections ne sont pas suivies.

Chaque tentative envoie le même corps. Seuls X-Relay-Timestamp et X-Relay-Signature changent.

Une livraison, tentative par tentativemessage.statusevt_01a1cf07f1a27b6e9d20c3a85e17f4b9
succeeded
  1. Tentative 109:12:44
    http_503Votre endpoint a répondu 503.
    X-Relay-Timestamp: 1794474764Prochaine tentative dans 30 s
  2. Tentative 209:13:14
    timeoutPas de réponse en 10 secondes.
    X-Relay-Timestamp: 1794474794Prochaine tentative dans 2 min
  3. Tentative 309:15:24
    200Votre endpoint a répondu 200.
    X-Relay-Timestamp: 1794474924Livrée
Le même corps et le même identifiant evt_ à chaque tentative.

Calendrier des relances

Chaque attente varie de 10 % au plus, dans un sens ou dans l'autre. Quand la tentative suivante tomberait plus de 24 heures après l'événement, la livraison se termine en échec.

TentativeAttenteEnviron, après l'événement
1Immédiate0 s
230 s30 s
32 min2 min 30 s
410 min12 min 30 s
530 min42 min 30 s
61 h1 h 42 min
72 h3 h 42 min
84 h7 h 42 min
96 h13 h 42 min
108 h21 h 42 min

Doublons

Les livraisons sont « au moins une fois ». L'id du corps (evt_…) et l'en-tête X-Relay-Delivery (dlv_…) restent identiques d'une relance ou d'une relivraison à l'autre : gardez les identifiants déjà traités et ignorez les répétitions. Deux endpoints qui reçoivent le même changement ont chacun leur livraison, avec son propre identifiant.

Désactivation automatique

Après 10 livraisons d'affilée terminées en échec, chacune après ses 24 heures de relances, l'endpoint est désactivé et vous recevez une notification dans l'application. Une coupure plus courte ne le déclenche jamais, et toute réussite remet le compteur à zéro.

Corrigez l'endpoint, puis réactivez-le depuis le tableau de bord ou avec enabled: true. Rien n'est envoyé tant qu'il est désactivé : relivrez les événements manqués depuis le journal des livraisons.

PATCH /v1/webhooks/endpoints/whk_01a1c5e0a9407e8db0c27f4a19d3e65b
Idempotency-Key: 7c2e4a90-1b3d-4f6e-8a5c-2d9b0e1f3a47
{ "enabled": true }

Relivrer et tester

Le journal des livraisons, dans le tableau de bord ou via GET /v1/webhooks/deliveries, montre chaque tentative et la réponse de votre endpoint. Relivrer une livraison terminée renvoie le même corps et le même identifiant, avec une nouvelle fenêtre de 24 heures ; la demande répond 409 webhook_delivery_pending tant que la livraison est encore en cours.

Un test envoie un événement webhook.test signé à un endpoint, même désactivé.

POST /v1/webhooks/deliveries/dlv_01a1cf07f1a27b6e9d20c3a85e17f4b9/redeliver
Idempotency-Key: 5b8d1f02-9c4e-4a7b-b6d3-0e2f8a1c7d94
HTTP/1.1 202 Accepted

Règles de l'endpoint

Une URL https:// sur l'internet public. Les hôtes privés, de bouclage, link-local et internes au cluster sont refusés à l'enregistrement (422 invalid_webhook_url) et revérifiés avant chaque envoi (destination_not_public, sans relance). Nous ne nous connectons qu'à l'adresse vérifiée, et les redirections ne sont pas suivies.

Ordre

Un webhook vous dit que quelque chose a changé. L'ordre d'arrivée n'est pas garanti.

  • Les événements peuvent arriver dans le désordre, surtout après des relances. Comparez created_at, ou relisez la ressource, avant d'écraser un état plus récent.
  • Le statut d'un message ne fait qu'avancer : pending → sent → delivered → read, ou failed. Ignorez un statut qui le ferait reculer.
  • message.reaction et group.participants portent la liste complète actuelle ; chat.updated, group.updated et number.status portent l'état après le changement. Gardez le plus récent selon created_at.

Envoyez-vous un événement de test.

Ajoutez votre endpoint dans Paramètres › Développeurs › Webhooks, envoyez un test et regardez-le arriver dans le journal des livraisons.