Savoir quel bouton ou quelle ligne le client a touché

Le clic arrive sur votre webhook en message.inbound, type text, avec le libellé touché dans text et votre identifiant dans button_id.

Savoir quel bouton ou quelle ligne le client a touché

Astuce
Le clic arrive sur votre webhook en message.inbound, type text, avec le libellé touché dans text et votre identifiant dans button_id.

Scénario

Vous avez envoyé des boutons, un menu en liste ou un template à boutons de réponse rapide. Le client touche un choix. Votre serveur doit savoir lequel pour continuer la conversation.

Ce qui arrive sur votre webhook

{ "event": "message.inbound",
  "data": { "from": "+237690000001", "contact_name": "Awa", "number": "100000000000001",
            "wa_message_id": "wamid.HBg…", "message_id": "9d3e…",
            "type": "text", "text": "Prendre RDV", "button_id": "rdv:new",
            "media_id": null, "media_url": null, "media_mime": null,
            "timestamp": "2026-10-30T09:14:07+00:00" },
  "timestamp": "2026-10-30T09:14:07+00:00" }
ChampContenu
type"text" pour un clic, comme pour un message tapé
textle libellé touché (titre du bouton ou de la ligne)
button_idl'id que vous avez donné au bouton ou à la ligne ; pour un bouton de réponse rapide de template, son libellé (Meta le renvoie comme payload) ; null si le client a tapé le texte lui-même
numberle numéro (phone_number_id) qui a reçu le clic : repassez-le en from pour répondre du même numéro

Code

Le gestionnaire vérifie la signature, puis aiguille sur button_id :

Node.js

// Express — receive the webhook, verify it, then react.
import express from "express";
import crypto from "node:crypto";

const app = express();
const SECRET = process.env.GESKAP_WEBHOOK_SECRET; // shown once when you register the webhook

// Keep the RAW bytes: the signature covers them, not a re-serialized JSON.
app.post("/webhooks/whatsapp", express.raw({ type: "application/json" }), (req, res) => {
  const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
  const got = req.get("X-Camairetech-Signature") || "";
  if (got.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))) {
    return res.sendStatus(401);
  }
  res.sendStatus(200); // answer fast: one attempt, 8 s timeout, no retry
  const { event, data } = JSON.parse(req.body.toString("utf8"));
  if (event === "message.inbound") onInbound(data); // your code
});

app.listen(3000);

PHP

<?php
// Receive the webhook, verify it, then react.
$raw = file_get_contents("php://input"); // raw bytes, before json_decode
$expected = "sha256=" . hash_hmac("sha256", $raw, getenv("GESKAP_WEBHOOK_SECRET"));
$got = $_SERVER["HTTP_X_CAMAIRETECH_SIGNATURE"] ?? "";
if (!hash_equals($expected, $got)) {
    http_response_code(401);
    exit;
}
http_response_code(200);
$payload = json_decode($raw, true);
if ($payload["event"] === "message.inbound") {
    on_inbound($payload["data"]); // your code
}

Python

# Flask — receive the webhook, verify it, then react.
import hashlib, hmac, json, os
from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["GESKAP_WEBHOOK_SECRET"].encode()

@app.post("/webhooks/whatsapp")
def whatsapp_webhook():
    raw = request.get_data()  # raw bytes, before any JSON parsing
    expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, request.headers.get("X-Camairetech-Signature", "")):
        abort(401)
    payload = json.loads(raw)
    if payload["event"] == "message.inbound":
        on_inbound(payload["data"])  # your code
    return "", 200

Dans onInbound : si data.button_id vaut rdv:new, lancez la prise de rendez-vous ; s'il est null, traitez data.text comme une saisie libre.

Attention
Piège : n'aiguillez jamais sur le libellé. Deux boutons peuvent porter le même titre (« Détails » sur deux produits), et un client peut taper le mot à la main. button_id est la seule clé fiable. Mettez-y ce dont vous avez besoin pour agir : order:1042, slot:2026-11-03T09:00.
Attention
Piège : il n'y a pas de type interactive ni button dans le webhook : un clic arrive en type: "text". Testez button_id, pas type.

Pas de webhook ? Les mêmes messages, avec leur texte, se relisent par GET /v1/messages/inbound (voir Archiver messages et médias).

Preuve — vérifié dans le code de l'API : meta_webhooks.py, webhooks.py, client_webhooks.py ; appliqué par la démo WhatsApp : webhook.