Notifications de commande WhatsApp depuis votre backend
Envoyez confirmations de commande, avis d'expédition et de livraison sur WhatsApp depuis votre backend : templates utilitaires, variables, statuts.
Pour notifier une commande sur WhatsApp, créez un template de catégorie UTILITY par étape
(confirmée, expédiée, livrée), faites-le approuver par Meta, puis appelez POST /api/v1/messages
depuis votre backend avec les variables de la commande. Un texte libre n'est permis que si le
client vous a écrit dans les 24 dernières heures.
Mis à jour le 8 octobre 2026
De quoi avez-vous besoin avant de commencer ?
- Un numéro connecté à Genuka WA et son
connectionId— voir le démarrage rapide. - Une clé API, utilisée uniquement côté serveur — voir Authentification.
- Un moyen de paiement sur le compte WhatsApp Business, chez Meta. Genuka est Meta Tech
Provider, pas BSP : Meta facture le compte directement, et un client onboardé par un Tech
Provider doit y ajouter son propre moyen de paiement
(Meta, Partners).
Sans lui, les templates sont approuvés mais chaque envoi revient en erreur
131042— voir erreur 131042.
Quels templates créer pour une boutique en ligne ?
Un template par moment où le client attend une nouvelle de sa commande. Trois suffisent pour commencer :
| Étape | Template | Variables du corps | Bouton |
|---|---|---|---|
| Commande confirmée | commande_confirmee | prénom, numéro, montant | URL « Voir ma commande » |
| Commande expédiée | commande_expediee | prénom, numéro, transporteur | URL « Suivre le colis » |
| Commande livrée | commande_livree | prénom, numéro | Aucun : le message invite à répondre |
Pour Meta, un template utilitaire est déclenché par une action ou une demande du client, lui est propre, et ne contient rien de promotionnel. Une confirmation de commande ou un avis d'expédition en sont les exemples types ; un template qui mêle l'information de commande à une offre, une vente additionnelle ou une incitation au renouvellement est reclassé en marketing (doc Meta, catégorisation). Gardez les promotions hors de ces messages.
Comment créer un template utilitaire ?
Soumettez le template
Chaque variable du corps exige un exemple dans example.body_text, et la variable d'un bouton URL
ne remplace que la fin de l'adresse, avec son propre exemple.
curl -X POST https://wa.genuka.com/api/v1/templates \
-H "Authorization: Bearer $GENUKA_WA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"connectionId": "con_1",
"name": "commande_expediee",
"language": "fr",
"category": "UTILITY",
"components": [
{ "type": "BODY",
"text": "Bonjour {{1}}, votre commande {{2}} est partie avec {{3}}. Suivez-la avec le bouton ci-dessous.",
"example": { "body_text": [["Awa", "CMD-1042", "DHL"]] } },
{ "type": "BUTTONS", "buttons": [
{ "type": "URL", "text": "Suivre le colis",
"url": "https://boutique.example.com/suivi/{{1}}",
"example": ["https://boutique.example.com/suivi/CMD-1042"] }
] }
]
}'Depuis le tableau de bord : Templates › Nouveau template, catégorie Utilitaire.
Attendez l'approbation
Le verdict de Meta arrive en webhook template_status (si votre endpoint est abonné à
template.status_changed, ou à tous les événements), se lit sur GET /api/v1/templates/{id},
et se rattrape avec POST /api/v1/templates/sync si un webhook s'est perdu. N'envoyez qu'un
template approved.
Fixez une durée de vie si le message périme
Si un message ne peut pas être livré, WhatsApp réessaie pendant la durée de vie du template :
30 jours par défaut pour un template utilitaire, réglable de 30 secondes à 12 heures
(doc Meta, time-to-live).
Un « votre livreur arrive dans 30 minutes » reçu le lendemain fait plus de mal que de bien :
pour ce genre de template, passez messageSendTtlSeconds à la création.
Comment envoyer la notification depuis votre backend ?
Un appel par notification. body remplit {{1}}, {{2}}, {{3}} dans l'ordre (variables est
un alias accepté), et buttons[].text complète l'URL du bouton.
const API = "https://wa.genuka.com/api/v1";
type Order = { number: string; firstName: string; phone: string; carrier: string };
/** Corps de la réponse : `data` en cas de succès, sinon `error` (et `meta` si Meta a refusé). */
type ApiResult = {
data?: { messageId: string };
error?: string;
message?: string;
meta?: { retryable?: boolean; code?: number };
};
export async function sendOrderShipped(order: Order): Promise<string> {
const response = await fetch(`${API}/messages`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.GENUKA_WA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
connectionId: process.env.GENUKA_WA_CONNECTION_ID,
to: order.phone, // international, avec le + : "+237699001122"
template: {
name: "commande_expediee",
language: "fr", // la langue exacte du template, sinon l'envoi vise "en_US"
body: [order.firstName, order.number, order.carrier],
buttons: [{ type: "url", text: order.number }],
},
}),
signal: AbortSignal.timeout(15_000),
});
// Un 5xx renvoyé par le CDN n'est pas du JSON : sans ce catch, l'erreur d'analyse perdrait
// `retryable` et la notification serait classée en échec définitif.
const result = (await response.json().catch(() => ({}))) as ApiResult;
if (!response.ok || !result.data) {
throw Object.assign(new Error(result.message ?? result.error), {
status: response.status,
// Un refus définitif (template, numéro) ne se réessaie pas : meta.retryable le dit.
retryable: response.status >= 500 || result.meta?.retryable === true,
});
}
return result.data.messageId;
}Un 200 veut dire que Meta a accepté le message ; la livraison se suit par webhook, avec ce
messageId. Pour envoyer le même template à une liste entière en une fois, passez plutôt par les
campagnes ; tous les types de contenu sont décrits dans
Envoyer des messages.
Quand pouvez-vous envoyer un message libre plutôt qu'un template ?
Quand le client vous écrit, une fenêtre de service de 24 heures s'ouvre, et chaque nouveau message de sa part la relance. Tant qu'elle est ouverte, vous pouvez répondre avec n'importe quel message ; une fois fermée, seuls les templates approuvés passent (doc Meta).
Le cas typique : le client répond à l'avis d'expédition « je ne serai pas là demain ». Votre
réponse peut être un simple texte, cité grâce à replyTo :
{
"connectionId": "con_1",
"to": "+237699001122",
"replyTo": "wamid.HBg…",
"text": "C'est noté, le livreur repassera jeudi entre 14 h et 16 h."
}Côté facture, Meta ne fait pas payer les messages hors template, et un template utilitaire livré pendant une fenêtre ouverte est gratuit lui aussi (tarifs Meta).
Hors fenêtre, un texte libre échoue, mais pas au moment de l'envoi. D'après nos essais sur un
numéro réel, Meta l'accepte avec un messageId, puis l'abandonne : le plus souvent, un statut
failed avec l'erreur 131047 suit
(codes d'erreur Meta) ;
parfois, aucun statut n'arrive. N'attendez donc pas l'échec : Genuka WA ne bloque pas l'envoi,
mais vous prévient dans la réponse, et c'est sur cet avertissement qu'il faut agir :
{
"data": {
"messageId": "wamid.HBg…",
"warning": { "code": "outside_service_window", "message": "…" }
}
}outside_service_window signifie que le dernier message du client date de plus de 24 heures :
renvoyez plutôt un template. unverified_service_window signifie seulement que nous n'avons aucun
message entrant de ce contact en mémoire, ce qui ne prouve pas que la fenêtre est fermée. Suivez
alors le statut du message, et ne renvoyez un template que si failed avec 131047 arrive, ou si
rien n'arrive : le renvoyer tout de suite risquerait d'envoyer deux fois la même notification.
Comment éviter d'envoyer deux fois la même notification ?
L'API n'a pas de clé d'idempotence : c'est à votre backend de garantir qu'une commande ne reçoit qu'une seule fois chaque notification. La façon la plus simple est une table dont la clé primaire est le couple commande + étape.
create table order_notifications (
order_id text not null,
event text not null, -- confirmed | shipped | delivered
status text not null default 'pending',
message_id text unique, -- le wamid renvoyé par Genuka WA
error_code integer,
created_at timestamptz not null default now(),
primary key (order_id, event)
);import { Pool } from "pg";
const db = new Pool();
export async function notifyOnce(orderId: string, event: string, send: () => Promise<string>) {
// 1. Réserver : un second appel pour la même commande et la même étape ne fait rien.
const claimed = await db.query(
"insert into order_notifications (order_id, event) values ($1, $2) on conflict do nothing",
[orderId, event],
);
if (claimed.rowCount === 0) return;
try {
// 2. Envoyer, 3. noter le wamid : c'est lui qui reliera les statuts à cette ligne.
const messageId = await send();
await db.query(
"update order_notifications set status = 'sent', message_id = $3 where order_id = $1 and event = $2",
[orderId, event, messageId],
);
} catch (error) {
if ((error as { retryable?: boolean }).retryable === true) {
// Libérer la réservation : le prochain passage du job réessaiera.
await db.query("delete from order_notifications where order_id = $1 and event = $2", [orderId, event]);
} else {
await db.query(
"update order_notifications set status = 'failed' where order_id = $1 and event = $2",
[orderId, event],
);
}
throw error;
}
}
// await notifyOnce(order.id, "shipped", () => sendOrderShipped(order));Un timeout réseau est ambigu : Meta a peut-être accepté le message avant que la réponse ne se perde. Ce code le classe en échec plutôt que de risquer un doublon. Si, pour votre boutique, un doublon rare vaut mieux qu'une confirmation perdue, traitez aussi les timeouts comme réessayables.
Comment suivre la livraison de chaque notification ?
Les statuts arrivent sur votre webhook sous le type message_status. data.id est le messageId
renvoyé à l'envoi, et data.status vaut sent, delivered, read ou failed. En cas d'échec,
data.errors[0].code porte le code Meta
(doc Meta, statuts).
const RANK: Record<string, number> = { pending: 0, sent: 1, delivered: 2, read: 3, failed: 4 };
if (event.type === "message_status") {
const { id: messageId, status, errors } = event.data;
const row = await findNotificationByMessageId(messageId);
// Les statuts peuvent arriver dans le désordre : un statut n'est jamais rétrogradé,
// et `failed` est définitif.
if (row && row.status !== "failed" && (RANK[status] ?? -1) > RANK[row.status]) {
await setNotificationStatus(messageId, status, errors?.[0]?.code ?? null);
}
}Pour Meta, read signifie que le message s'est affiché dans une conversation ouverte sur
l'appareil du client ; ne bâtissez pas de logique métier qui l'attend. La vérification de la
signature et la déduplication sont détaillées dans
Recevoir messages et statuts par webhook.
Quelles erreurs rencontrerez-vous le plus souvent ?
| Erreur | Où la lire | Signification | Que faire |
|---|---|---|---|
400 missing_fields | Réponse | connectionId ou to absent | Compléter la requête |
400 recipient_is_sender | Réponse | to est le numéro qui envoie | Changer de destinataire |
402 subscription_past_due | Réponse | L'essai ou la période payée est échu : rien ne part avant le renouvellement | Renouveler la formule, puis renvoyer ce que notifyOnce a classé failed |
402 plan_limit_messages | Réponse | Quota de messages de la période épuisé | Ajouter des numéros ou changer de formule |
404 connection_not_found | Réponse | Le connectionId n'existe pas ou n'est pas à la portée de votre clé | Vérifier l'identifiant et la clé |
409 number_released | Réponse | Le numéro a été libéré de votre forfait | Le reconnecter par l'Embedded Signup |
403 account_deactivated | Réponse | Le client propriétaire du numéro est suspendu | Le réactiver depuis sa fiche dans le tableau de bord |
132001 | meta.code ou statut failed | Template absent dans cette langue, ou non approuvé | Vérifier nom, langue et statut |
132000 | meta.code ou statut failed | Nombre de variables différent de celui du template | Aligner body sur {{1}}…{{n}} |
131026 | Statut failed | Message non délivrable, par exemple numéro sans WhatsApp | Basculer vers l'email ou le SMS |
131047 | Statut failed, parfois jamais reçu | Texte libre envoyé plus de 24 h après le dernier message du client | Envoyer un template ; agir sur le warning de la réponse |
130429 | meta.code | Débit de la Cloud API atteint | Réessayer avec un délai croissant |
131042 | meta.code ou statut failed | Problème de moyen de paiement sur le compte Meta | Le client règle sa facturation côté Meta — voir erreur 131042 |
Les significations des codes Meta viennent de leur liste des codes d'erreur.
FAQ
Combien coûte une notification de commande sur WhatsApp ?
Meta facture les templates livrés selon leur catégorie et le pays du destinataire, directement sur le compte WhatsApp Business du marchand ; un template utilitaire livré pendant une fenêtre de service ouverte est gratuit (tarifs Meta). Genuka WA ne prend aucune marge sur ces tarifs : vous payez un abonnement par numéro, avec un quota de messages — voir les formules.
Puis-je glisser un code promo dans la confirmation de commande ?
Non. Une offre ou une vente additionnelle dans un template utilitaire le fait reclasser en marketing par Meta. Envoyez la promotion à part, dans un template marketing, aux clients qui ont accepté d'en recevoir.
Faut-il le consentement du client ?
Oui. Meta demande d'indiquer clairement que la personne accepte de recevoir des messages de votre entreprise, et de nommer cette entreprise (doc Meta, opt-in). Une case « Recevoir le suivi de ma commande de la part de [Nom de la boutique] sur WhatsApp » au moment du paiement, enregistrée avec la commande, remplit ces deux conditions.
Que faire si le client n'a pas WhatsApp ?
Le statut revient failed avec le code 131026. Ne réessayez pas : envoyez la même information
par email ou SMS.
Je gère plusieurs boutiques : comment choisir le numéro d'envoi ?
Chaque boutique connecte son numéro et obtient son connectionId ; c'est lui qui décide d'où part
le message. Une clé partenaire atteint toutes vos boutiques, une clé client une seule — voir
Authentification et, pour retrouver le numéro d'une boutique,
Connecter un numéro.