# Envoyer un code OTP WhatsApp depuis Node.js

URL: https://wa.genuka.com/docs/guides/whatsapp-otp-nodejs
Language: French

> 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](https://wa.genuka.com/docs/quickstart).
* Une clé API, utilisée **uniquement côté serveur** — voir [Authentification](https://wa.genuka.com/docs/authentication).
* 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](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview)).
  Sans lui, le template est approuvé mais chaque code revient en erreur `131042` — voir
  [erreur 131042](https://wa.genuka.com/docs/errors/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](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/authentication-templates/authentication-templates)).

| É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](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/authentication-templates/copy-code-button-authentication-templates)).

> [!NOTE]
> **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](https://wa.genuka.com/docs/api#templates). Commencez
> par `COPY_CODE`.

## Comment créer le template d'authentification ?

1. ### Soumettez le template

   Un seul appel, une fois pour toutes. Notez la langue choisie : l'envoi devra reprendre exactement
   la même.

   **curl**

   ```bash title="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" }
         ] }
       ]
     }'
   ```

   **Node.js**

   ```ts title="create-otp-template.ts"
   const response = await fetch("https://wa.genuka.com/api/v1/templates", {
     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,
       name: "code_connexion",
       language: "fr",
       category: "AUTHENTICATION",
       // Au-delà de 5 minutes, Meta abandonne la livraison : le code aurait expiré de toute façon.
       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" }],
         },
       ],
     }),
   });

   console.log(response.status, await response.json());
   ```

   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 ».

2. ### 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.

**Node.js**

```ts title="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;
}
```

**curl**

```bash title="POST /api/v1/messages"
curl -X POST https://wa.genuka.com/api/v1/messages \
  -H "Authorization: Bearer $GENUKA_WA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "connectionId": "con_1",
    "to": "+237699001122",
    "template": { "name": "code_connexion", "language": "fr", "otp": "482913" }
  }'
```

```json title="Réponse"
{ "data": { "messageId": "wamid.HBg…" } }
```

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`.

> [!WARNING]
> **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](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/supported-languages)) :
> 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](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes)).

## Comment vérifier le code saisi ?

La vérification ne passe jamais par WhatsApp : elle se fait entièrement chez vous.

```ts title="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é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](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/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](https://wa.genuka.com/docs/guides/receive-messages-webhooks) pour la
vérification de signature.

```ts title="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](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/status)).

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](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/authentication-templates/authentication-best-practices)).
* **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 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](https://wa.genuka.com/docs/errors/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](https://docs.360dialog.com/docs/resources/authentication-messages)).
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](https://wa.genuka.com/docs/errors/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](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits)).

## 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](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing)),
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](https://wa.genuka.com/#pricing).

### 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](https://developers.facebook.com/documentation/business-messaging/whatsapp/getting-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](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/authentication-templates/authentication-templates)
* [Meta — Copy code authentication templates](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/authentication-templates/copy-code-button-authentication-templates)
* [Meta — Time-to-live](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/time-to-live)
* [Meta — Authentication best practices](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/authentication-templates/authentication-best-practices)
* [Meta — Supported languages](https://developers.facebook.com/documentation/business-messaging/whatsapp/templates/supported-languages)
* [Meta — Status messages webhook](https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/status)
* [Meta — Getting opt-in](https://developers.facebook.com/documentation/business-messaging/whatsapp/getting-opt-in)
* [Meta — Error codes](https://developers.facebook.com/documentation/business-messaging/whatsapp/support/error-codes)
* [Meta — Partners (Tech Providers et Solution Partners)](https://developers.facebook.com/documentation/business-messaging/whatsapp/solution-providers/overview)
* [Meta — Messaging limits](https://developers.facebook.com/documentation/business-messaging/whatsapp/messaging-limits)
* [Meta — Pricing](https://developers.facebook.com/documentation/business-messaging/whatsapp/pricing)
* [360dialog — Authentication messages](https://docs.360dialog.com/docs/resources/authentication-messages)
