Envoyer un code de vérification (OTP) par WhatsApp

Pas avec un template OTP natif aujourd'hui : l'API ne sait ni le créer ni y glisser le code. Ce qui marche : un code en texte libre dans les 24 h après que le client vous a écrit, ou mieux un lien unique. Vous générez et vérifiez le code vous-même.

Envoyer un code de vérification (OTP) par WhatsApp

Astuce
Pas avec un template OTP natif aujourd'hui : l'API ne sait ni le créer ni y glisser le code. Ce qui marche : un code en texte libre dans les 24 h après que le client vous a écrit, ou mieux un lien unique. Vous générez et vérifiez le code vous-même.

Scénario

Vous voulez vérifier le numéro d'un utilisateur ou le connecter : vous lui envoyez un code à 6 chiffres, il le saisit chez vous.

Réponse honnête : le template OTP natif de WhatsApp (catégorie AUTHENTICATION, bouton « Copier le code ») n'est pas utilisable de bout en bout avec cette API aujourd'hui. Voici ce que le code fait, et ce qui marche à la place.

Ce que fait l'API avec la catégorie AUTHENTICATION

Conclusion : ne comptez pas sur POST /v1/messages pour un OTP natif. Ne contournez pas en déguisant un code en template d'une autre catégorie : le code ne l'empêche pas, mais Meta peut refuser ou requalifier le template, et rien ici ne garantit qu'il passe.

Ce qui marche : vous générez et vérifiez le code

L'API est un relais : le code, sa durée de vie et sa vérification restent chez vous.

Client  : Connexion
Webhook : message.inbound  from=+237690000001  text="Connexion"
Serveur : code 482913 créé, valable 10 min, 3 essais, lié au numéro
Vous    : « Votre code Boutique Exemple : 482913. Valable 10 minutes. Ne le partagez jamais. »
Client  : (saisit 482913 sur votre site ou le renvoie dans la conversation)
  1. Le client écrit d'abord (un bouton « Connexion » sur votre site peut ouvrir WhatsApp avec ce mot). Sa fenêtre de 24 h est ouverte : vous répondez en texte libre.

curl

curl -X POST "https://wa-api.geskap.com/v1/messages/reply" \
  -H "Authorization: Bearer $GESKAP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "to": "+237690000001",
  "text": "Votre code Boutique Exemple : 482913. Valable 10 minutes. Ne le partagez jamais."
}'

Node.js

const res = await fetch("https://wa-api.geskap.com/v1/messages/reply", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.GESKAP_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "to": "+237690000001",
    "text": "Votre code Boutique Exemple : 482913. Valable 10 minutes. Ne le partagez jamais."
  }),
});
console.log(res.status, await res.json());

PHP

$ch = curl_init("https://wa-api.geskap.com/v1/messages/reply");
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",
    "text" => "Votre code Boutique Exemple : 482913. Valable 10 minutes. Ne le partagez jamais.",
  ]),
]);
$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/reply",
    headers={"Authorization": f"Bearer {os.environ['GESKAP_API_KEY']}"},
    json={
        "to": "+237690000001",
        "text": "Votre code Boutique Exemple : 482913. Valable 10 minutes. Ne le partagez jamais.",
    },
    timeout=15,
)
print(res.status_code, res.json())
  1. Votre serveur stocke le code haché, avec son numéro, une expiration et un compteur d'essais :

Node.js

// Your server — generate, store hashed, verify. Nothing of this runs on our side.
import crypto from "node:crypto";

const sha = (s) => crypto.createHash("sha256").update(s).digest("hex");

export function issueCode(phone) {
  const code = String(crypto.randomInt(0, 1_000_000)).padStart(6, "0");
  db.otp.upsert(phone, { hash: sha(code), expiresAt: Date.now() + 10 * 60_000, triesLeft: 3 });
  return code; // then POST /v1/messages/reply with this code
}

export function checkCode(phone, code) {
  const row = db.otp.get(phone);
  if (!row || row.expiresAt < Date.now() || row.triesLeft <= 0) return false;
  row.triesLeft -= 1;
  const ok = sha(code) === row.hash;
  if (ok) db.otp.delete(phone); // single use
  return ok;
}
  1. Hors des 24 h, le texte libre est refusé (400, « Hors de la fenêtre de 24h »). Une personne qui n'a jamais écrit à votre numéro ne peut donc pas recevoir un code par cette voie : voir Écrire à un client qui ne vous a jamais écrit.
Attention
Piège : un code en clair dans la conversation reste lisible dans l'historique du téléphone. Pour une action sensible (solde, paiement), préférez un lien unique et un PIN saisi sur votre page : le secret ne passe pas par WhatsApp.
Attention
Piège : associez toujours le code au numéro from qui l'a demandé et vérifiez les deux. Voir Reconnaître un client à son numéro. Limitez les essais et la durée (10 minutes, 3 essais ici).
Attention
Piège : POST /v1/messages/reply n'a pas d'idempotency_key (un champ en trop est ignoré sans erreur) : un rejeu réseau enverrait deux codes. Dédoublonnez de votre côté, par exemple sur le wa_message_id du message entrant qui a déclenché l'envoi (voir Idempotence).

Si votre besoin est l'OTP natif (bouton « Copier le code », hors 24 h), écrivez au support Geskap au +237 6 56 09 88 31 avec votre cas : il n'est pas disponible en libre-service. Voir aussi Limites, honnêtement.

Preuve — vérifié dans le code de l'API : api_quota.py, templates.py, api_schemas.py, cloud_api.py, api_send.py, WHATSAPP_DEV_API.md.