Répondre par une FAQ, puis par une IA, puis passer la main à un humain

Oui — vous branchez votre propre LLM sur le webhook message.inbound ; l'ordre FAQ, puis IA, puis humain est de votre code, et le « bot qui se tait » aussi (un drapeau par numéro, levé par « menu »).

Répondre par une FAQ, puis par une IA, puis passer la main à un humain

Astuce
Oui — vous branchez votre propre LLM sur le webhook message.inbound ; l'ordre FAQ, puis IA, puis humain est de votre code, et le « bot qui se tait » aussi (un drapeau par numéro, levé par « menu »).

Scénario

Un client pose une question. Votre serveur répond dans cet ordre : (1) une réponse écrite d'avance si la question est dans votre FAQ, (2) sinon votre LLM, avec vos consignes et vos informations, (3) et si le client demande un humain, ou si l'IA ne sait pas, vous passez la main : le bot se tait pour ce client jusqu'à ce qu'il écrive « menu ».

Oui, c'est faisable avec la Dev-API, et sans template tant que le client a écrit il y a moins de 24 h (chaque question le garantit). Le point à comprendre : l'API n'a pas d'IA intégrée dans ce parcours et ne décide de rien. Elle vous remet chaque message sur votre webhook et envoie ce que vous lui dites d'envoyer ; l'aiguillage FAQ, IA, humain est votre code. Le branchement d'un LLM est détaillé dans Brancher le webhook sur n8n, Zapier, un CRM ou un LLM.

Conversation

Client  : Vous ouvrez à quelle heure ?
Vous    : Du lundi au samedi, 7h30 à 18h.          (FAQ)
          [ Menu ]  [ Un conseiller ]
Client  : Vous faites des devis pour un mariage de 80 personnes ?
Vous    : (votre LLM répond avec vos tarifs et conditions)
Client  : Je veux parler à quelqu'un
Vous    : Un conseiller prend le relais et vous répond ici même.
          Tapez « menu » pour revenir à l'assistant.
          — le bot ne répond plus à ce client —
Client  : menu
Vous    : Bonjour ! Que souhaitez-vous faire ?     (le bot reprend)

Code

Réponse FAQ avec deux boutons de sortie :

curl

curl -X POST "https://wa-api.geskap.com/v1/messages/interactive" \
  -H "Authorization: Bearer $GESKAP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "to": "+237690000001",
  "body": "Du lundi au samedi, 7h30 à 18h.",
  "buttons": [
    { "id": "menu", "title": "Menu" },
    { "id": "human", "title": "Un conseiller" }
  ]
}'

Node.js

const res = await fetch("https://wa-api.geskap.com/v1/messages/interactive", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.GESKAP_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "to": "+237690000001",
    "body": "Du lundi au samedi, 7h30 à 18h.",
    "buttons": [
      { "id": "menu", "title": "Menu" },
      { "id": "human", "title": "Un conseiller" }
    ]
  }),
});
console.log(res.status, await res.json());

PHP

$ch = curl_init("https://wa-api.geskap.com/v1/messages/interactive");
curl_setopt_array($ch, [
  CURLOPT_CUSTOMREQUEST => "POST",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    "Authorization: Bearer " . getenv("GESKAP_API_KEY"),
    "Content-Type: application/json",
  ],
  CURLOPT_POSTFIELDS => json_encode([
    "to" => "+237690000001",
    "body" => "Du lundi au samedi, 7h30 à 18h.",
    "buttons" => [
      ["id" => "menu", "title" => "Menu"],
      ["id" => "human", "title" => "Un conseiller"],
    ],
  ]),
]);
$res = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $res;

Python

import os, requests

res = requests.request(
    "POST",
    "https://wa-api.geskap.com/v1/messages/interactive",
    headers={"Authorization": f"Bearer {os.environ['GESKAP_API_KEY']}"},
    json={
        "to": "+237690000001",
        "body": "Du lundi au samedi, 7h30 à 18h.",
        "buttons": [
            {"id": "menu", "title": "Menu"},
            {"id": "human", "title": "Un conseiller"},
        ],
    },
    timeout=15,
)
print(res.status_code, res.json())

Aiguillage côté serveur, appelé par le gestionnaire du webhook :

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
async function onInbound(data) {
  const text = (data.text || "").trim().toLowerCase();
  const session = await db.getSession(data.from);

  // 3. un humain tient la conversation : le bot se tait, sauf « menu »
  if (session.handoff) {
    if (text !== "menu") return;                       // votre équipe répond de son côté
    await db.setHandoff(data.from, false);
  }
  if (data.button_id === "human" || /conseiller|humain|agent/.test(text)) {
    await db.setHandoff(data.from, true);              // le bot se tait désormais
    await notifyTeam(data.from, data.text);            // alerte votre équipe
    return sendText(data.from, "Un conseiller prend le relais. Tapez « menu » pour revenir à l'assistant.");
  }
  // 1. FAQ : réponse écrite d'avance
  const hit = FAQ.find(([re]) => re.test(text));
  if (hit) return sendFaqButtons(data.from, hit[1]);
  // 2. IA : votre LLM, avec vos consignes
  const answer = await askYourLLM(data.text);
  if (answer) return sendText(data.from, answer);
  // l'IA ne sait pas : proposez l'humain
  return sendFaqButtons(data.from, "Je préfère vous passer un conseiller.");
}

sendText est un POST /v1/messages/reply (Répondre en texte libre) et sendFaqButtons le POST /v1/messages/interactive ci-dessus. L'application de démonstration suit exactement cet ordre : option touchée, puis FAQ, puis mots-clés, puis IA ; le relais conseiller met un drapeau et ne répond plus tant que le client n'a pas écrit « menu ».

Limites exactes

ÉlémentLimite
Réponse texte libre (text)4 096 caractères
Message à boutons (body)1 024 caractères, 3 boutons de 20 caractères
Coût d'une réponse1 crédit pour 4 messages par défaut ; recevoir : 1 000 messages gratuits par mois, puis 1 crédit pour 20
Délai pour répondre24 h après le dernier message du client
Attention
Piège : « le bot se tait » n'est pas un réglage de l'API : si vous ne mettez pas de drapeau par numéro dans votre base, votre code continuera de répondre en même temps que votre conseiller. L'API ne fait pas non plus la différence entre votre bot et un humain qui écrit : les deux passent par les mêmes appels.
Attention
Piège : un LLM peut inventer. Ne le laissez pas annoncer un prix, un stock ou un statut de paiement qu'il n'a pas lu dans vos données, et prévoyez la sortie vers l'humain quand il hésite. Voir Questions que pose un assistant IA.

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