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 »).
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 »).
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.
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)
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 ».
| Élément | Limite |
|---|---|
| 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éponse | 1 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épondre | 24 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.