Genuka WA docs

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 fetch natif.
  • 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émentCe que vous choisissezRègle Meta
CorpsRien — texte prédéfini, le code y est inséréPas de texte personnalisé
Mention de sécuritéadd_security_recommendation: trueOptionnelle
Pied de pagecode_expiration_minutesDe 1 à 90 minutes
BoutonOTP avec otp_type: COPY_CODELibellé de 25 caractères maximum
CodeGénéré par votre serveur15 caractères maximum
Durée de vie (TTL)messageSendTtlSecondsMeta : 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.

POST /api/v1/templates
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_status dont data.event vaut APPROVED ou REJECTED, 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.

otp.ts
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.

otp.ts (suite)
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éeOù elle se règleCe qu'elle fait
code_expiration_minutesPied de page du templateCe que l'utilisateur lit : « expire dans 5 minutes »
messageSendTtlSecondsTemplateAu-delà, Meta cesse de tenter la livraison
Expiration serveurVotre baseLa 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.

Dans votre récepteur de webhooks (signature déjà vérifiée)
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 delivered ni de read aprè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 MetaSignificationRéaction
131026Message non délivrable, par exemple un numéro sans WhatsAppBasculer vers le SMS sans réessayer
131056Trop de messages vers ce même destinataire en peu de tempsAttendre avant de renvoyer
130429Débit de la Cloud API atteintRéessayer avec un délai croissant
132001Template absent dans cette langue ou non approuvéCorriger le nom ou la langue
131042Problème de moyen de paiement sur le compte Meta du client : aucun code ne partBasculer 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_rejected avec 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_status du 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

Sur cette page