Envoyer un code OTP WhatsApp depuis Node.js
Envoyez des codes OTP par WhatsApp depuis Node.js avec Genuka WA : template d'authentification, bouton copier le code, expiration et repli SMS.
Pour envoyer un code OTP par WhatsApp depuis Node.js avec Genuka WA, créez une fois un template
de catégorie AUTHENTICATION avec un bouton « Copier le code », puis appelez
POST /api/v1/messages avec le raccourci otp. Meta impose le texte, vous fournissez seulement
le code ; votre serveur garde l'expiration et la vérification. Prérequis souvent oublié :
l'entreprise doit avoir passé la vérification d'entreprise Meta (ou un autre parcours de montée en
charge de Meta).
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.
- Node.js 22 ou plus récent : une version LTS encore maintenue, avec
fetchnatif. - Une entreprise qui a passé la vérification d'entreprise Meta (ou un autre parcours de montée en charge de Meta). C'est la condition que l'on découvre trop tard : sans elle, Meta refuse de créer le template, avec un message d'erreur qui accuse l'application plutôt que l'entreprise. La dernière section de ce guide y revient.
- 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, le template est approuvé mais chaque code revient en erreur
131042— voir erreur 131042.
Comment fonctionne un code OTP sur WhatsApp ?
Un code à usage unique passe par un template de catégorie AUTHENTICATION. Contrairement aux
autres catégories, vous n'en rédigez pas le texte : Meta fournit un corps prédéfini dans lequel il
insère votre code, et vous ne choisissez que les options autour
(doc Meta).
| Élément | Ce que vous choisissez | Règle Meta |
|---|---|---|
| Corps | Rien — texte prédéfini, le code y est inséré | Pas de texte personnalisé |
| Mention de sécurité | add_security_recommendation: true | Optionnelle |
| Pied de page | code_expiration_minutes | De 1 à 90 minutes |
| Bouton | OTP avec otp_type: COPY_CODE | Libellé de 25 caractères maximum |
| Code | Généré par votre serveur | 15 caractères maximum |
| Durée de vie (TTL) | messageSendTtlSeconds | Meta : 10 min par défaut, de 30 s à 15 min ; Genuka WA accepte de 60 à 600 s |
Les URL, les médias et les emojis ne sont pas acceptés dans ce type de template (doc Meta, bouton copier le code).
Copier le code, saisie automatique ou zéro clic ?
COPY_CODE fonctionne sur tous les téléphones : l'utilisateur touche le bouton et colle le code.
ONE_TAP et ZERO_TAP remplissent le code dans votre application Android et exigent son
package_name et son signature_hash — voir la référence API. Commencez
par COPY_CODE.
Comment créer le template d'authentification ?
Soumettez le template
Un seul appel, une fois pour toutes. Notez la langue choisie : l'envoi devra reprendre exactement la même.
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": "code_connexion",
"language": "fr",
"category": "AUTHENTICATION",
"messageSendTtlSeconds": 300,
"components": [
{ "type": "BODY", "add_security_recommendation": true },
{ "type": "FOOTER", "code_expiration_minutes": 5 },
{ "type": "BUTTONS", "buttons": [
{ "type": "OTP", "otp_type": "COPY_CODE", "text": "Copier le code" }
] }
]
}'La réponse 201 contient le template enregistré, avec son id et son status. Depuis le tableau
de bord, le même résultat s'obtient par Templates › Nouveau template, catégorie
Authentification, bouton « Copier le code ».
Attendez l'approbation
Meta examine le template. Le verdict vous parvient de trois façons :
- un webhook
template_statusdontdata.eventvautAPPROVEDouREJECTED, si votre endpoint est abonné àtemplate.status_changed(ou à tous les événements) ; GET /api/v1/templates/{id}, qui renvoie le statut et son historique ;POST /api/v1/templates/sync, qui relit les statuts chez Meta si un webhook s'est perdu.
Seul un template approved peut être envoyé.
Comment envoyer le code depuis Node.js ?
Le raccourci otp place le code aux deux endroits que Meta exige pour un template
d'authentification : le paramètre du corps et celui du bouton. Vous n'écrivez pas les
components vous-même.
import crypto from "node:crypto";
const API = "https://wa.genuka.com/api/v1";
const CODE_TTL_MS = 5 * 60_000; // aligné sur code_expiration_minutes et messageSendTtlSeconds
const MAX_ATTEMPTS = 5;
type OtpEntry = { codeHash: string; expiresAt: number; attempts: number; messageId: 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?: { errorClass?: string; retryable?: boolean; code?: number };
};
// En mémoire pour l'exemple. En production : Redis ou votre base, avec la même expiration,
// sinon un second serveur ne connaît pas le code émis par le premier.
const store = new Map<string, OtpEntry>();
const hash = (phone: string, code: string) =>
crypto.createHash("sha256").update(`${phone}:${code}`).digest("hex");
export async function sendOtp(phone: string): Promise<string> {
// Générateur cryptographique, jamais Math.random().
const code = crypto.randomInt(0, 1_000_000).toString().padStart(6, "0");
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: phone, // format international avec le +, ex. "+237699001122"
// `language` toujours explicite : sans lui, l'envoi vise "en_US".
template: { name: "code_connexion", language: "fr", otp: code },
}),
});
// Un 5xx du CDN n'est pas du JSON : sans ce catch, l'erreur de parsing masquerait le statut.
const body = (await response.json().catch(() => ({}))) as ApiResult;
if (!response.ok || !body.data) {
// body.meta.errorClass dit qui peut corriger : voir « Que faire si le code n'arrive pas ? »
throw new Error(`OTP non envoyé : ${response.status} ${body.error ?? ""} — ${body.message ?? ""}`);
}
// Un nouveau code remplace le précédent : un seul code valide par numéro à la fois.
store.set(phone, {
codeHash: hash(phone, code),
expiresAt: Date.now() + CODE_TTL_MS,
attempts: 0,
messageId: body.data.messageId,
});
return body.data.messageId;
}Un 200 signifie que Meta a accepté le message, pas qu'il est arrivé. La livraison se confirme
par webhook, avec le même messageId.
La langue doit correspondre au caractère près
Pour Meta, en et en_US sont deux codes de langue distincts
(langues prises en charge) :
un template approuvé en en n'existe pas en en_US. Envoyer dans une langue où le template
n'existe pas, ou n'est pas approuvé, échoue avec l'erreur 132001
(codes d'erreur Meta).
Comment vérifier le code saisi ?
La vérification ne passe jamais par WhatsApp : elle se fait entièrement chez vous.
export function verifyOtp(phone: string, input: string): boolean {
const entry = store.get(phone);
if (!entry || Date.now() > entry.expiresAt) return false;
// Limiter les essais : un code à 6 chiffres se devine en 1 000 000 tentatives au pire,
// beaucoup moins si personne ne compte.
if (entry.attempts >= MAX_ATTEMPTS) return false;
entry.attempts += 1;
// Deux empreintes SHA-256 en hexadécimal : même longueur, comparaison en temps constant.
const ok = crypto.timingSafeEqual(
Buffer.from(entry.codeHash, "hex"),
Buffer.from(hash(phone, input.trim()), "hex"),
);
if (ok) store.delete(phone); // usage unique
return ok;
}Quatre règles tiennent l'ensemble : un code par numéro à la fois, un usage unique, un nombre d'essais borné, une expiration décidée par votre serveur.
Comment régler l'expiration et la durée de vie ?
Trois durées coexistent. Alignez-les sur la même valeur.
| Durée | Où elle se règle | Ce qu'elle fait |
|---|---|---|
code_expiration_minutes | Pied de page du template | Ce que l'utilisateur lit : « expire dans 5 minutes » |
messageSendTtlSeconds | Template | Au-delà, Meta cesse de tenter la livraison |
| Expiration serveur | Votre base | La seule qui fait foi au moment de vérifier |
Quand un message ne peut pas être livré, WhatsApp réessaie pendant la durée de vie du template,
puis l'abandonne ; si aucun statut delivered ni read n'est arrivé avant la fin de ce délai,
considérez le message comme perdu
(doc Meta, time-to-live).
Un code livré après son expiration est pire qu'un code jamais livré : l'utilisateur le saisit, il
est refusé, et il ne comprend pas pourquoi.
Que faire si le code n'arrive pas ?
Suivez le message par son messageId dans votre récepteur de webhooks — voir
Recevoir messages et statuts par webhook pour la
vérification de signature.
if (event.type === "message_status") {
const { id: messageId, status, errors } = event.data;
// `read` vaut livraison : Meta n'envoie parfois pas de `delivered` (voir ci-dessous).
if (status === "delivered" || status === "read") await markOtpDelivered(messageId);
if (status === "failed") await markOtpUndeliverable(messageId, errors?.[0]?.code);
}N'attendez pas uniquement delivered. Quand l'utilisateur a la conversation ouverte au moment où
le message arrive — le cas typique d'un code attendu —, Meta le considère livré et lu d'un coup et
n'envoie que read
(doc Meta, statuts).
Puis, côté interface :
- Échec explicite : proposez immédiatement un autre canal. Meta recommande d'ailleurs de laisser l'utilisateur choisir entre WhatsApp, l'email et le SMS (bonnes pratiques Meta).
- Pas de
deliveredni dereadaprès une trentaine de secondes : affichez « Recevoir le code par SMS » plutôt que de renvoyer un second code WhatsApp au même numéro. - Renvoi : imposez un délai entre deux demandes, et invalidez le code précédent à chaque envoi.
| Code Meta | Signification | Réaction |
|---|---|---|
131026 | Message non délivrable, par exemple un numéro sans WhatsApp | Basculer vers le SMS sans réessayer |
131056 | Trop de messages vers ce même destinataire en peu de temps | Attendre avant de renvoyer |
130429 | Débit de la Cloud API atteint | Réessayer avec un délai croissant |
132001 | Template absent dans cette langue ou non approuvé | Corriger le nom ou la langue |
131042 | Problème de moyen de paiement sur le compte Meta du client : aucun code ne part | Basculer vers le SMS ; le client règle sa facturation chez Meta — voir erreur 131042 |
Quand Meta refuse l'envoi sur le moment, ou qu'un contrôle du template ou du numéro le bloque
avant l'appel, la réponse est un 400 (409 pour un problème de configuration) dont error vaut
send_<classe>, avec un objet meta qui porte errorClass et le code Meta. La classe tranche :
recipient_permanent (changer de canal), retryable ou recipient_throttled (réessayer plus
tard), template ou config (le problème est chez vous, pas chez l'utilisateur — alertez
l'équipe). Les refus propres à Genuka WA n'ont pas d'objet meta : lisez error.
400 missing_fields et 400 recipient_is_sender sont des erreurs dans la requête. Les autres
bloquent tous les codes tant que le compte n'est pas corrigé : 402 subscription_past_due (l'essai
ou la période payée est échu), 402 plan_limit_messages (le quota de la période est épuisé),
409 number_released, 403 account_deactivated et 404 connection_not_found. Dans tous ces cas,
envoyez le code par SMS ou par email sans attendre et alertez l'équipe : l'utilisateur patiente sur
l'écran de connexion.
Pourquoi Meta refuse-t-il de créer mon template d'authentification ?
Meta réserve la catégorie AUTHENTICATION aux comptes qui remplissent deux conditions : avoir
franchi un de ses parcours de montée en charge — en pratique la vérification d'entreprise Meta ou
une vérification menée par un partenaire — et disposer d'une limite d'envoi d'au moins 2 000
(360dialog, documentation des messages d'authentification).
Les catégories MARKETING et UTILITY ne sont pas concernées.
Voici ce que nous constatons sur les comptes connectés à Genuka WA quand l'entreprise n'est pas vérifiée :
- la création répond
meta_rejectedavec le message de Graph « Application does not have permission for this action ». Il désigne l'application, mais la cause est l'entreprise du client : aucun réglage de clé ou de permission n'y changera rien ; - le
health_statusdu compte WhatsApp Business porte l'erreur 141010, « The Business has not passed business verification ».
La solution est de terminer la vérification d'entreprise dans le Meta Business Suite du client, puis de recréer le template. Le détail est sur la page erreur 141010.
Un indice se lit aussi dans GET /api/v1/numbers/{connectionId}/health : le champ
messagingLimitTier. Un portefeuille d'entreprise récent démarre à 250 destinataires uniques par
24 heures hors fenêtre de service ; la vérification d'entreprise est l'un des chemins qui le font
passer à 2 000
(doc Meta, limites d'envoi).
FAQ
Peut-on personnaliser le texte du message OTP ?
Non. Le corps d'un template d'authentification est fixé par Meta ; vous réglez seulement la mention de sécurité, la durée affichée dans le pied de page et le libellé du bouton.
Combien coûte un code OTP envoyé par WhatsApp ?
Meta facture les templates livrés selon leur catégorie et le pays du destinataire (tarifs Meta), directement sur le compte WhatsApp Business du client. Genuka WA n'ajoute aucune marge : chaque envoi compte simplement dans le quota de messages de votre abonnement — voir les formules.
L'utilisateur doit-il m'avoir écrit avant de recevoir un code ?
Non : un template peut être envoyé hors de la fenêtre de 24 heures. Il faut en revanche l'accord de la personne, et Meta pose deux conditions : indiquer clairement qu'elle accepte de recevoir des messages de votre entreprise, et nommer cette entreprise (doc Meta, opt-in). Sur l'écran de connexion, une mention « Recevoir mon code de connexion [Nom de l'entreprise] sur WhatsApp » à côté du champ du numéro remplit les deux.
Que se passe-t-il si le numéro n'a pas WhatsApp ?
Le statut revient failed avec le code 131026. Ne réessayez pas sur WhatsApp : proposez le SMS
ou l'email.
Sources
- Meta — Authentication templates
- Meta — Copy code authentication templates
- Meta — Time-to-live
- Meta — Authentication best practices
- Meta — Supported languages
- Meta — Status messages webhook
- Meta — Getting opt-in
- Meta — Error codes
- Meta — Partners (Tech Providers et Solution Partners)
- Meta — Messaging limits
- Meta — Pricing
- 360dialog — Authentication messages