Écrire à un client qui ne m'a jamais écrit, ou plus de 24 h après son dernier message

Oui, mais seulement avec un template approuvé par Meta : vous le créez (POST /v1/templates), attendez APPROVED, puis l'envoyez (POST /v1/messages). Un message libre hors des 24 h est impossible.

Écrire à un client qui ne m'a jamais écrit, ou plus de 24 h après son dernier message

Astuce
Oui, mais seulement avec un template approuvé par Meta : vous le créez (POST /v1/templates), attendez APPROVED, puis l'envoyez (POST /v1/messages). Un message libre hors des 24 h est impossible.

Scénario

Vous voulez prévenir un client qui ne vous a pas écrit dans les dernières 24 h (ou jamais) : colis prêt, rendez-vous demain, facture disponible.

Oui, c'est possible, mais jamais en texte libre. WhatsApp n'autorise un message libre que dans les 24 h qui suivent le dernier message du client. En dehors, il faut un template : un modèle de message que Meta a approuvé à l'avance. Le parcours tient en trois appels : créer, attendre APPROVED, envoyer.

Conversation

Vous    : (création du template « colis_pret », statut PENDING)
Meta    : (approbation, relue par GET /v1/templates → APPROVED)
Vous    : POST /v1/messages  to=+237690000001  variables=["Awa", "CMD-1042"]
Client  : reçoit « Bonjour Awa, votre commande CMD-1042 est prête à être retirée. »

Code

  1. Créez le template. Il part chez Meta avec le statut PENDING :

curl

curl -X POST "https://wa-api.geskap.com/v1/templates" \
  -H "Authorization: Bearer $GESKAP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "colis_pret",
  "language": "fr",
  "category": "UTILITY",
  "body_text": "Bonjour {{1}}, votre commande {{2}} est prête à être retirée.",
  "example_values": ["Awa", "CMD-1042"],
  "footer_text": "Boutique Exemple"
}'

Node.js

const res = await fetch("https://wa-api.geskap.com/v1/templates", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.GESKAP_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "name": "colis_pret",
    "language": "fr",
    "category": "UTILITY",
    "body_text": "Bonjour {{1}}, votre commande {{2}} est prête à être retirée.",
    "example_values": ["Awa", "CMD-1042"],
    "footer_text": "Boutique Exemple"
  }),
});
console.log(res.status, await res.json());

PHP

$ch = curl_init("https://wa-api.geskap.com/v1/templates");
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([
    "name" => "colis_pret",
    "language" => "fr",
    "category" => "UTILITY",
    "body_text" => "Bonjour {{1}}, votre commande {{2}} est prête à être retirée.",
    "example_values" => ["Awa", "CMD-1042"],
    "footer_text" => "Boutique Exemple",
  ]),
]);
$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/templates",
    headers={"Authorization": f"Bearer {os.environ['GESKAP_API_KEY']}"},
    json={
        "name": "colis_pret",
        "language": "fr",
        "category": "UTILITY",
        "body_text": "Bonjour {{1}}, votre commande {{2}} est prête à être retirée.",
        "example_values": ["Awa", "CMD-1042"],
        "footer_text": "Boutique Exemple",
    },
    timeout=15,
)
print(res.status_code, res.json())
  1. Relisez la liste jusqu'à voir APPROVED (ou REJECTED avec rejected_reason) :

curl

curl -X GET "https://wa-api.geskap.com/v1/templates" \
  -H "Authorization: Bearer $GESKAP_API_KEY"

Node.js

const res = await fetch("https://wa-api.geskap.com/v1/templates", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.GESKAP_API_KEY}`,
  },
});
console.log(res.status, await res.json());

PHP

$ch = curl_init("https://wa-api.geskap.com/v1/templates");
curl_setopt_array($ch, [
  CURLOPT_CUSTOMREQUEST => "GET",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    "Authorization: Bearer " . getenv("GESKAP_API_KEY"),
  ],
]);
$res = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $res;

Python

import os, requests

res = requests.request(
    "GET",
    "https://wa-api.geskap.com/v1/templates",
    headers={"Authorization": f"Bearer {os.environ['GESKAP_API_KEY']}"},
    timeout=15,
)
print(res.status_code, res.json())
  1. Envoyez-le. Les variables remplissent {{1}}, {{2}}… dans l'ordre :

curl

curl -X POST "https://wa-api.geskap.com/v1/messages" \
  -H "Authorization: Bearer $GESKAP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "to": "+237690000001",
  "template": {
    "name": "colis_pret",
    "language": "fr",
    "variables": ["Awa", "CMD-1042"]
  },
  "idempotency_key": "colis-CMD-1042"
}'

Node.js

const res = await fetch("https://wa-api.geskap.com/v1/messages", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.GESKAP_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "to": "+237690000001",
    "template": {
      "name": "colis_pret",
      "language": "fr",
      "variables": ["Awa", "CMD-1042"]
    },
    "idempotency_key": "colis-CMD-1042"
  }),
});
console.log(res.status, await res.json());

PHP

$ch = curl_init("https://wa-api.geskap.com/v1/messages");
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",
    "template" => [
      "name" => "colis_pret",
      "language" => "fr",
      "variables" => ["Awa", "CMD-1042"],
    ],
    "idempotency_key" => "colis-CMD-1042",
  ]),
]);
$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",
    headers={"Authorization": f"Bearer {os.environ['GESKAP_API_KEY']}"},
    json={
        "to": "+237690000001",
        "template": {
            "name": "colis_pret",
            "language": "fr",
            "variables": ["Awa", "CMD-1042"],
        },
        "idempotency_key": "colis-CMD-1042",
    },
    timeout=15,
)
print(res.status_code, res.json())

La réponse est un 202 : le message est accepté, pas encore livré. Le statut de livraison (sent, delivered, read, failed) arrive sur votre webhook message.status ou se relit par GET /v1/messages/{id}.

Ce qui peut refuser l'envoi

CodeCause
400template inconnu, pas encore APPROVED, ou nombre de variables incorrect
402solde de crédits insuffisant
403catégorie MARKETING non activée sur votre compte, ou template non autorisé pour cette clé
429débit ou quota dépassé (en-tête Retry-After)
502Meta a refusé l'envoi : les crédits sont remboursés
Attention
Piège : un template est propre à chaque numéro WhatsApp. Approuvé sur un numéro, il n'existe pas sur un autre : créez-le sur chacun (?number= à la création, from à l'envoi). Voir Plusieurs numéros.
Attention
Piège : la catégorie compte. UTILITY (une information liée à une commande ou un rendez-vous) s'envoie tout de suite. MARKETING (promo, relance commerciale) répond 403 tant que l'envoi marketing n'est pas activé sur votre compte : voir Marketing et promo.
Attention
Piège : si le client répond, il rouvre la fenêtre de 24 h : vous pouvez alors continuer en texte libre ou avec des boutons, sans nouveau template.

Si Meta refuse le template, voir Template refusé : les causes. Pour ne pas payer pendant vos essais : Tester sans payer. Coût : Coût d'un message. Pour ne jamais envoyer deux fois au même client : Idempotence.

Preuve — vérifié dans le code de l'API : public_api.py, api_schemas.py, api_send.py.