Proposer des créneaux avec les places restantes et éviter la double réservation

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.

Proposer des créneaux avec les places restantes et éviter la double réservation

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.

Scénario

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.

Conversation

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).

Code

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.

Limites exactes

ÉlémentLimite
Lignes proposées10 au total : au-delà, filtrez (jour, praticien) avant
Titre d'une ligne24 caractères : « Zumba · 18:00 »
Description d'une ligne72 caractères : « Coach B · 3 places restantes »
id d'une ligne200 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.