Faire remplir un formulaire pas à pas dans WhatsApp

Oui — une question par message, les réponses gardées dans votre base (l'API ne retient aucun état), « annuler » à tout moment, et un drapeau d'urgence pour les demandes à traiter en priorité.

Faire remplir un formulaire pas à pas dans WhatsApp

Astuce
Oui — une question par message, les réponses gardées dans votre base (l'API ne retient aucun état), « annuler » à tout moment, et un drapeau d'urgence pour les demandes à traiter en priorité.

Scénario

Un client veut une consultation. Vous lui posez 4 questions, une par message, en boutons ou en liste quand les choix sont connus, en texte libre pour le récit. À la fin, vous créez le dossier chez vous, avec un drapeau « urgent » si le client a une échéance proche.

Oui, c'est possible sans template, dans les 24 h : chaque réponse du client rouvre la fenêtre. Il n'y a pas de « formulaire » côté API : c'est votre serveur qui garde l'état (à quelle question en est ce numéro) et envoie la suivante. C'est la logique de l'application de démonstration, en production.

Conversation

Client  : Je voudrais voir un avocat
Vous    : De quel domaine s'agit-il ?   [ Voir les domaines ]  (liste)
Client  : (touche « Droit du travail »)
Vous    : Vous êtes plutôt…   [ Un particulier ]  [ Une entreprise ]
Client  : (touche « Un particulier »)
Vous    : Décrivez votre situation en quelques phrases.   [ Annuler ]
Client  : Licencié sans préavis, l'employeur refuse de payer mes indemnités.
Vous    : Avez-vous une date limite ?
          [ Moins de 7 jours ]  [ Ce mois-ci ]  [ Pas de date ]
Client  : (touche « Moins de 7 jours »)
Vous    : Situation transmise (réf. DEM-17). Vous serez rappelé aujourd'hui.

Code

Envoi d'une étape à choix (les étapes à texte libre utilisent le texte libre) :

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": "Avez-vous une date limite (audience, convocation, délai) ?",
  "footer": "Tapez « annuler » pour arrêter",
  "buttons": [
    { "id": "echeance:urgent", "title": "Moins de 7 jours" },
    { "id": "echeance:mois", "title": "Ce mois-ci" },
    { "id": "echeance:aucune", "title": "Pas de date" }
  ]
}'

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": "Avez-vous une date limite (audience, convocation, délai) ?",
    "footer": "Tapez « annuler » pour arrêter",
    "buttons": [
      { "id": "echeance:urgent", "title": "Moins de 7 jours" },
      { "id": "echeance:mois", "title": "Ce mois-ci" },
      { "id": "echeance:aucune", "title": "Pas de date" }
    ]
  }),
});
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" => "Avez-vous une date limite (audience, convocation, délai) ?",
    "footer" => "Tapez « annuler » pour arrêter",
    "buttons" => [
      ["id" => "echeance:urgent", "title" => "Moins de 7 jours"],
      ["id" => "echeance:mois", "title" => "Ce mois-ci"],
      ["id" => "echeance:aucune", "title" => "Pas de date"],
    ],
  ]),
]);
$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": "Avez-vous une date limite (audience, convocation, délai) ?",
        "footer": "Tapez « annuler » pour arrêter",
        "buttons": [
            {"id": "echeance:urgent", "title": "Moins de 7 jours"},
            {"id": "echeance:mois", "title": "Ce mois-ci"},
            {"id": "echeance:aucune", "title": "Pas de date"},
        ],
    },
    timeout=15,
)
print(res.status_code, res.json())

Moteur minimal côté serveur : un état par numéro, une étape à la fois.

// state[from] = { step: 0, answers: {} } — dans VOTRE base, pas en mémoire
const STEPS = [
  { key: "domaine", ask: sendDomainList },
  { key: "qualite", ask: sendQualiteButtons },
  { key: "resume",  ask: (to) => sendText(to, "Décrivez votre situation en quelques phrases."),
    validate: (t) => t.trim().length < 15 ? "Un peu plus de détails, s'il vous plaît." : null },
  { key: "echeance", ask: sendEcheanceButtons },
];

async function onInbound(data) {
  const s = (await db.getState(data.from)) ?? null;
  if (!s) return;                                    // pas de formulaire en cours
  if (/^(annuler|stop)$/i.test(data.text.trim())) {  // « annuler » à tout moment
    await db.clearState(data.from);
    return sendText(data.from, "C'est annulé. Écrivez-moi quand vous voulez.");
  }
  const step = STEPS[s.step];
  const value = data.button_id ? data.button_id.split(":")[1] : data.text; // choix = fin de button_id, récit = text
  const err = step.validate?.(value);
  if (err) return sendText(data.from, err);          // on redemande la même étape
  s.answers[step.key] = value;
  if (s.step + 1 < STEPS.length) {
    s.step += 1; await db.saveState(data.from, s);
    return STEPS[s.step].ask(data.from);
  }
  const urgent = s.answers.echeance === "urgent";    // drapeau d'urgence
  await db.createRequest({ phone: data.from, answers: s.answers, urgent });
  await db.clearState(data.from);
  await sendText(data.from, urgent ? "Transmis : réponse aujourd'hui." : "Transmis : réponse sous 24 h ouvrées.");
}

Dans la démonstration, les étapes peuvent aussi être conditionnelles (une étape est sautée selon les réponses précédentes), chaque étape affiche « Tapez « annuler » pour arrêter », et la réponse finale n'est pas la même pour une demande urgente (« en priorité », « aujourd'hui »).

Limites exactes

ÉlémentLimite
Choix par étape3 en boutons, 10 en liste
Texte d'une question (body)1 024 caractères en interactif, 4 096 en texte libre
État du formulaireaucun côté API : à stocker chez vous, par numéro
Fenêtre entre deux réponses24 h : chaque réponse du client la relance
Attention
Piège : l'API ne se souvient pas de l'étape. Si vous ne stockez pas l'état par numéro, un « Oui » tapé au milieu du formulaire est pris pour une nouvelle conversation. Donnez à chaque id un préfixe d'étape (qualite:particulier) et ignorez un id qui ne correspond pas à l'étape en cours (vieux bouton).
Attention
Piège : un client qui abandonne laisse un formulaire ouvert. Prévoyez « annuler » (ci-dessus) et une expiration de l'état (par exemple 24 h) : au-delà, la fenêtre est fermée et seule une template peut le relancer. Ne posez jamais de donnée sensible (mot de passe, pièce d'identité) : voir Données sensibles derrière un lien + PIN.

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