Webhooks d'événements

Recevez message.status et message.inbound en temps réel, signés HMAC-SHA256. Vérifiez toujours la signature.

Webhooks

Plutôt que de sonder GET /v1/messages/{id}, enregistrez UN endpoint HTTPS et nous y postons les événements en temps réel : mises à jour de statut de vos envois, et messages entrants de vos clients. Nous ne faisons tourner aucun bot sur votre numéro — nous vous relayons l'entrant.

Enregistrer le webhook

POST /v1/console/webhook

Attention — Le secret n'est affiché qu'une fois
À l'enregistrement, le secret de signature est renvoyé une seule fois — stockez-le. Un GET ultérieur renvoie { url, configured } sans jamais le secret ; DELETE désactive le webhook.

Format de livraison

Chaque événement est un POST vers votre URL. La livraison est best-effort (~8 s de timeout, pas de retries automatiques pour l'instant) : répondez 2xx rapidement et faites le gros du travail de façon asynchrone.

En-tête de signature

X-Camairetech-Signature: sha256=<hex hmac-sha256 du corps brut>
Note — Chaque envoi est journalisé — surveillez le vôtre
Comme il n'y a pas de retry automatique, une livraison échouée vers votre URL est silencieuse pour vous. La console (Logs, catégorie « Webhook ») et GET /v1/logs?category=webhook journalisent chaque envoi vers votre endpoint avec son statut (succès/échec) et la raison de l'échec — c'est le seul moyen de savoir qu'un événement n'est jamais arrivé chez vous. Les 6 autres catégories (Envois, Réception, Crédits, Rejets, Clés, Numéros) couvrent le reste de l'activité de votre compte.

Types d'événements

message.status

{
  "event": "message.status",
  "data": {
    "id": "6f1c…",
    "wa_message_id": "wamid…",
    "status": "delivered",
    "recipient_masked": "2376****3456",
    "error": null
  },
  "timestamp": "2026-08-04T09:14:07.123456+00:00"
}

message.inbound

{
  "event": "message.inbound",
  "data": {
    "from": "+237699123456",
    "contact_name": "Jean Mballa",
    "number": "1234567890",
    "wa_message_id": "wamid…",
    "type": "text",
    "text": "Bonjour",
    "media_id": null,
    "media_url": null
  },
  "timestamp": "…"
}

type vaut text ou un type média (image, audio, video, document, sticker). media_id est l'identifiant Meta du média ; quand le média est hébergé, media_url est une URL absolue (GET /v1/media/{id}, redirection 302 vers une URL signée éphémère, conservée pour votre fenêtre de rétention) — null si rien n'est encore hébergé ou si l'hébergement est désactivé pour votre compte. number est le phone_number_id qui a reçu le message — repassez-le en from sur POST /v1/messages/reply pour répondre depuis le même numéro. Le même message est aussi stocké et relisible via GET /v1/messages/inbound — voir « Recevoir des messages ».

message.echo

{
  "event": "message.echo",
  "data": {
    "to": "+237699123456",
    "number": "1234567890",
    "wa_message_id": "wamid…",
    "type": "text",
    "text": "Bonjour",
    "media_id": null
  },
  "timestamp": "…"
}

message.echo est le pendant sortant de message.inbound : vous (ou un coéquipier) avez envoyé ce message à ce contact depuis l'application WhatsApp Business elle-même (Coexistence), pas via cette API — nous le relayons pour que votre historique reste complet. to est le contact destinataire ; number est le phone_number_id qui a envoyé le message. Média : métadonnées seules (media_id + media_mime, jamais de media_url) — ce média n'est pas téléchargeable depuis Meta. Même dualité stockage/push que message.inbound : relisible via GET /v1/messages/inbound?direction=echo (le champ direction distingue in de echo).

Vérifier la signature (toujours)

Recalculez le HMAC sur les octets bruts du corps (avant tout parsing JSON) et comparez à l'en-tête ; rejetez en cas d'écart. Re-sérialiser le corps changerait la signature.

Vérification (Python)

import hmac, hashlib

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or "")

Vérification (Node.js)

const crypto = require("crypto");
function verify(rawBody, header, secret) {
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header || ""));
}