Webhooks
Recevoir les messages entrants, les accusés de livraison et les changements de statut.
Vous déclarez une URL, Genuka WA y livre chaque événement en POST, signé.
Déclarer un endpoint
Dans le portail, section Webhooks : renseignez l'URL (HTTPS obligatoire) et cochez les
événements souhaités. Le secret de signature (whsec_…) s'affiche une seule fois à la création.
Un endpoint peut couvrir tous les numéros du compte (général), une seule entreprise connectée, ou un seul numéro — et une entreprise peut être exclue d'un endpoint général.
Événements disponibles
| Événement | Contenu |
|---|---|
messages | Messages entrants et statuts des messages envoyés |
message_template_status_update | Un template est approuvé, rejeté ou mis en pause |
account_update | Changement sur le compte : vérification, restriction, suspension |
phone_number_quality_update | La qualité ou le palier d'envoi d'un numéro a changé |
La plateforme ne conserve pas d'inbox : les webhooks sont la façon dont vous lisez les réponses.
Le payload
Chaque livraison est un POST portant un seul événement. data est la valeur Meta brute, intacte ;
les champs autour indiquent à quelle entreprise et à quel numéro elle se rapporte.
{
"id": "dlv_2f8c1a...",
"type": "message_status",
"field": "messages",
"created_at": "2026-08-04T09:31:07.412Z",
"partner_id": "cl9x...",
"company_id": "cm31...",
"connection_id": "cn77...",
"waba_id": "102290129340398",
"phone_number_id": "106540352242922",
"data": {
"id": "wamid.HBgLMjM3...",
"status": "delivered",
"timestamp": "1785836801",
"recipient_id": "237699000000"
}
}En-têtes
| En-tête | Contenu |
|---|---|
X-Genuka-Signature | La signature — voir ci-dessous |
X-Genuka-Event | Type d'événement, ex. message_status |
X-Genuka-Event-Field | Le champ webhook Meta sous-jacent |
X-Genuka-Delivery | Id de livraison, identique entre les tentatives — utilisez-le pour dédupliquer |
X-Genuka-Webhook-Id | L'endpoint visé |
X-Genuka-Attempt | Numéro de tentative, à partir de 1 |
Vérifier la signature
L'en-tête est X-Genuka-Signature, au format t=1785836801,v1=5f3c…, où v1 est le HMAC-SHA256
hexadécimal de `${t}.${corpsBrut}` calculé avec votre secret.
Le timestamp est inclus dans le calcul du HMAC : c'est ce qui empêche de rejouer une requête
capturée. Un récepteur rejette tout t en dehors de sa fenêtre de tolérance (300 secondes par
défaut), et t ne peut pas être modifié sans invalider v1.
Signez le corps brut, tel que reçu. Un JSON.parse suivi d'un JSON.stringify change
l'ordre des clés et les espaces : la signature ne correspondra jamais.
import { verifySignature } from "@genuka/whatsapp/webhooks";
export async function POST(request: Request) {
const raw = await request.text();
const signature = request.headers.get("x-genuka-signature");
if (!(await verifySignature(process.env.WEBHOOK_SECRET!, raw, signature))) {
return new Response("invalid signature", { status: 401 });
}
// Traitez en asynchrone, répondez tout de suite.
void handle(JSON.parse(raw));
return new Response("ok");
}Version minimale : @genuka/whatsapp 0.1.1
verifySignature reconnaît les deux schémas et applique l'algorithme correspondant : le format
horodaté de Genuka WA (t=…,v1=…) et celui de Meta (X-Hub-Signature-256), utile seulement si
vous recevez aussi des webhooks Meta en direct. En 0.1.0, seul le format Meta était géré — la
vérification d'un webhook Genuka WA échouait systématiquement.
Si vous n'utilisez pas la librairie, l'algorithme tient en quelques lignes :
import crypto from "node:crypto";
const TOLERANCE_SECONDS = 300;
export function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const timestamp = Number(parts.t);
// Rejette tout ce qui sort de la fenêtre : c'est ce qui rend une requête
// capturée inutilisable en rejeu.
if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(parts.v1, "utf8"),
Buffer.from(expected, "utf8"),
);
}Régénérez la clé au moindre doute
La régénération prend effet immédiatement : l'ancienne clé cesse de vérifier dès que vous régénérez, mettez donc votre récepteur à jour dans la foulée.
Trois règles de traitement
Répondez 200 immédiatement
Faites votre traitement en tâche de fond. Un endpoint lent déclenche des relivraisons, qui amplifient la charge exactement au mauvais moment.
Les événements peuvent être dupliqués. Le même message peut vous être livré deux fois. Rendez
votre traitement idempotent en vous appuyant sur l'identifiant du message (wamid) ou sur
X-Genuka-Delivery.
L'ordre n'est pas garanti. Une confirmation de lecture peut arriver avant l'accusé de livraison du
même message. Ne régressez jamais un statut : l'ordre logique est sent → delivered → read,
et failed est terminal.
Statuts d'un message envoyé
| Statut | Signification |
|---|---|
sent | Accepté par les serveurs WhatsApp |
delivered | Arrivé sur l'appareil du destinataire |
read | Lu — seulement si l'utilisateur a activé les confirmations de lecture |
failed | Échec, avec le code d'erreur Meta |
deleted | Message supprimé |
Reprises et échecs
Une livraison est réussie sur tout 2xx. Tout le reste — y compris un dépassement des 10 secondes —
est réessayé 5 fois avec un délai croissant (1 min, 5 min, 30 min, 2 h, 6 h), puis marqué en échec.
Journal et rejeu
Le portail conserve l'historique des livraisons : payload, statut HTTP, nombre de tentatives et réponse de votre serveur. En cas d'incident de votre côté, vous pouvez rejouer un événement une fois votre endpoint rétabli.
Une clé API ne sert pas à recevoir les webhooks : la seule chose à vérifier côté réception est la signature. Ne vous fiez jamais au contenu d'un événement non signé.