Genuka WA docs

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énementContenu
messagesMessages entrants et statuts des messages envoyés
message_template_status_updateUn template est approuvé, rejeté ou mis en pause
account_updateChangement sur le compte : vérification, restriction, suspension
phone_number_quality_updateLa 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.

POST https://votre-app.com/webhooks/whatsapp
{
  "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êteContenu
X-Genuka-SignatureLa signature — voir ci-dessous
X-Genuka-EventType d'événement, ex. message_status
X-Genuka-Event-FieldLe champ webhook Meta sous-jacent
X-Genuka-DeliveryId de livraison, identique entre les tentatives — utilisez-le pour dédupliquer
X-Genuka-Webhook-IdL'endpoint visé
X-Genuka-AttemptNumé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 :

verify-signature.ts
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é

StatutSignification
sentAccepté par les serveurs WhatsApp
deliveredArrivé sur l'appareil du destinataire
readLu — seulement si l'utilisateur a activé les confirmations de lecture
failedÉchec, avec le code d'erreur Meta
deletedMessage 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é.

Sur cette page