Oui — une liste de créneaux libres avec « 8 places restantes » en description de ligne ; à la validation, vous revérifiez côté serveur avant d'enregistrer, car la liste envoyée peut être périmée.
Astuce
Oui — une liste de créneaux libres avec « 8 places restantes » en description de ligne ; à la validation, vous revérifiez côté serveur avant d'enregistrer, car la liste envoyée peut être périmée.
Un client veut réserver. Vous lui envoyez la liste des créneaux encore libres ; chaque ligne affiche en description les places restantes (ou le nom du praticien). Il touche une ligne. Avant d'enregistrer, vous revérifiez que le créneau est toujours libre : entre l'envoi de la liste et le clic, quelqu'un d'autre a pu le prendre.
Oui, c'est possible sans template, dans les 24 h : c'est un menu en liste dont vous calculez le contenu. L'API ne gère pas votre agenda : les créneaux et les places vivent dans votre base, vous les calculez à chaque envoi.
Client : Je veux réserver un cours
Vous : Quel cours ? [ Voir les cours ]
- Cardio · 07:00 Coach A · 12 places restantes
- Zumba · 18:00 Coach B · 3 places restantes
- Yoga · 19:30 complet
Client : (touche « Zumba · 18:00 »)
Webhook : message.inbound type=text button_id="slot:zumba-1800"
Vous : (vous relisez les places chez vous : il en reste 3, vous enregistrez)
Place réservée (réf. CRS-482).
Envoi de la liste (les lignes sont produites par votre code, 10 au maximum) :
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": "Quel cours ?",
"list": {
"button": "Voir les cours",
"sections": [
{
"title": "Aujourd'\''hui",
"rows": [
{
"id": "slot:cardio-0700",
"title": "Cardio · 07:00",
"description": "Coach A · 12 places restantes"
},
{
"id": "slot:zumba-1800",
"title": "Zumba · 18:00",
"description": "Coach B · 3 places restantes"
}
]
}
]
}
}'
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": "Quel cours ?",
"list": {
"button": "Voir les cours",
"sections": [
{
"title": "Aujourd'hui",
"rows": [
{
"id": "slot:cardio-0700",
"title": "Cardio · 07:00",
"description": "Coach A · 12 places restantes"
},
{
"id": "slot:zumba-1800",
"title": "Zumba · 18:00",
"description": "Coach B · 3 places restantes"
}
]
}
]
}
}),
});
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" => "Quel cours ?",
"list" => [
"button" => "Voir les cours",
"sections" => [
[
"title" => "Aujourd'hui",
"rows" => [
[
"id" => "slot:cardio-0700",
"title" => "Cardio · 07:00",
"description" => "Coach A · 12 places restantes",
],
[
"id" => "slot:zumba-1800",
"title" => "Zumba · 18:00",
"description" => "Coach B · 3 places restantes",
],
],
],
],
],
]),
]);
$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": "Quel cours ?",
"list": {
"button": "Voir les cours",
"sections": [
{
"title": "Aujourd'hui",
"rows": [
{
"id": "slot:cardio-0700",
"title": "Cardio · 07:00",
"description": "Coach A · 12 places restantes",
},
{
"id": "slot:zumba-1800",
"title": "Zumba · 18:00",
"description": "Coach B · 3 places restantes",
},
],
},
],
},
},
timeout=15,
)
print(res.status_code, res.json())
Réception du choix : revérifiez avant d'enregistrer, dans une transaction de votre base.
const slotId = data.button_id?.startsWith("slot:") ? data.button_id.slice(5) : null;
if (slotId) {
const ok = await db.transaction(async (tx) => {
const left = await tx.placesLeft(slotId); // relu MAINTENANT, pas lu à l'envoi
if (left <= 0) return false;
await tx.book(slotId, data.from); // réservation atomique
return true;
});
if (!ok) await sendSlotList(data.from, "Ce créneau vient d'être pris, voici ce qui reste :");
else await sendText(data.from, "Place réservée.");
}
Dans l'application de démonstration, la liste n'affiche que les créneaux libres (Créneaux libres) et affiche places restantes ou complet en description ; à la validation, le cours est recompté et, s'il est complet, le client reçoit « Désolé, ce cours est complet. Choisissez un autre créneau. » avec un bouton pour recommencer.
| Élément | Limite |
|---|---|
| Lignes proposées | 10 au total : au-delà, filtrez (jour, praticien) avant |
| Titre d'une ligne | 24 caractères : « Zumba · 18:00 » |
| Description d'une ligne | 72 caractères : « Coach B · 3 places restantes » |
| id d'une ligne | 200 caractères : slot:zumba-1800, ou slot:2026-11-03T09:00 |
Attention
Piège : la liste que vous avez envoyée reste affichée dans la conversation. Le client peut toucher une ligne une heure plus tard, ou un autre client a pu prendre la dernière place entre-temps : les « places restantes » affichées ne sont pas une réservation. Revérifiez toujours à la validation, de façon atomique (transaction ou verrou sur votre base), sinon deux clients obtiennent le même créneau.
Attention
Piège : l'API n'a pas de calendrier : elle ne sait pas qu'un créneau est complet. Aucun champ « désactivé » ou « grisé » sur une ligne : retirez la ligne, ou écrivez « complet » en description et refusez à la validation.
Preuve — vérifié dans le code de l'API : api_schemas.py, public_api.py, webhooks.py ; appliqué par la démo WhatsApp : sante, sport.