Documentation

API Officielle WhatsApp

Comment connecter votre numéro à la plateforme de Meta, envoyer des messages et des modèles, et recevoir les événements dans votre système.

Mis à jour le 12 novembre 2026

Sommaire

Vue d'ensemble

L'API Officielle relie votre numéro WhatsApp à la plateforme de Meta (WhatsApp Cloud API) et vous donne une API simple pour envoyer des messages et recevoir ce que vos clients écrivent. Elle fonctionne avec n8n, avec votre CRM ou avec votre propre code.

Ce que vous recevez :

  • une clé d'accès pour envoyer des messages et des modèles, et consulter l'état de vos numéros ;

  • un webhook : chaque message reçu et chaque changement d'état arrivent à l'adresse de votre choix, signés ;

  • l'espace client, où vous connectez le numéro, créez des modèles de message et suivez votre consommation.

Toutes les requêtes vont vers :

https://wa.automate-ai.digital

Les messages sont facturés par Meta, directement sur votre compte (voir Coûts). L'abonnement couvre le numéro et l'API.

Démarrer

1. Connecter le numéro

Dans l'espace client, sous Mes produits, cliquez sur Connecter le numéro officiel et suivez le parcours de Meta : choisissez ou créez le compte WhatsApp Business, confirmez le numéro et autorisez la connexion. À la fin, le numéro apparaît comme connecté.

Meta demande deux choses :

  • la vérification de l'entreprise auprès de Meta, pour sortir des limites d'envoi initiales ;

  • un moyen de paiement sur le compte WhatsApp Business : sans lui, Meta cesse d'envoyer les messages.

2. Configurer le webhook et la clé

Dans l'espace client, sous Intégration technique :

  1. Webhook : indiquez l'adresse qui recevra les événements. L'espace client affiche une seule fois le secret du webhook, qui sert à vérifier la signature. Conservez-le.

  2. Clé d'accès : générez la clé. Elle n'apparaît, elle aussi, qu'une seule fois.

3. Utiliser le flux n8n prêt à l'emploi

Si vous utilisez n8n, le même endroit propose le bouton Télécharger le flux n8n. Le flux vérifie déjà la signature, répond aux messages et envoie des modèles. Importez-le, activez-le, collez son URL de production à l'étape Webhook, puis collez le secret et la clé là où le flux l'indique.

Authentification

Chaque requête porte la clé dans l'en-tête x-api-key :

curl https://wa.automate-ai.digital/instances/ID_DU_NUMERO \
  -H "x-api-key: wak_xxxxxxxx_xxxxxxxx"

Toutes les réponses ont la même forme :

{ "ok": true, "data": { "...": "..." } }
{ "ok": false, "error": { "code": "INSTANCE_NOT_FOUND", "message": "…", "retryable": false } }

L'ID_DU_NUMERO (instanceId) figure dans chaque événement reçu par votre webhook.

Une nouvelle clé invalide la précédente dès sa génération. Tout ce qui utilisait l'ancienne cesse de fonctionner jusqu'à sa mise à jour.

Envoyer des messages

curl -X POST https://wa.automate-ai.digital/instances/ID_DU_NUMERO/messages \
  -H "x-api-key: $CLE" -H "content-type: application/json" \
  -d '{
    "to": { "kind": "contact", "id": "33612345678" },
    "content": { "type": "text", "body": "Bonjour ! Votre commande est en route." },
    "idempotencyKey": "commande-1234-confirmation"
  }'
  • to.id est le numéro au format international, sans + ni espaces.

  • idempotencyKey (facultatif, recommandé) : si la même requête est répétée avec la même clé, le message ne part pas deux fois.

  • replyTo (facultatif) : le providerMessageId du message auquel vous répondez, pour l'afficher en citation.

La réponse est 202 avec l'identifiant du message :

{ "ok": true, "data": { "messageId": "44444444-…", "queuedAt": "2026-11-12T10:00:00.000Z" } }

202 signifie que le message est entré dans la file, pas qu'il a été livré. Le résultat arrive ensuite sur le webhook, sous forme de message.status (voir État des envois).

Types de contenu

type

Champs

text

body (jusqu'à 4096 caractères)

image

media, caption

video

media, caption

audio

media

document

media, filename, caption

location

latitude, longitude, name, address

buttons

body, buttons : jusqu'à 3, chacun avec id et title (jusqu'à 20 caractères)

media vaut { "kind": "url", "url": "https://…" }, ou { "kind": "base64", "data": "…", "mimeType": "image/png" }.

Boutons de réponse

{
  "to": { "kind": "contact", "id": "33612345678" },
  "content": {
    "type": "buttons",
    "body": "Confirmez-vous le rendez-vous de demain à 15 h ?",
    "buttons": [
      { "id": "confirmer", "title": "Confirmer" },
      { "id": "reporter", "title": "Reporter" }
    ]
  }
}

Quand le client appuie sur un bouton, vous recevez un message.received dont le content.type est interactive_reply, avec le même id dans replyId. C'est sur lui que votre flux s'appuie pour décider.

Fenêtre de 24 heures et modèles

C'est la règle la plus importante du canal officiel :

  • dans les 24 heures qui suivent le dernier message du client, vous pouvez envoyer des messages libres (/messages) et des modèles ;

  • en dehors de cette fenêtre, uniquement des modèles approuvés par Meta (/messages/template).

Une réponse à un message que vous venez de recevoir est toujours dans la fenêtre. Pour lancer une conversation (rappel, confirmation, avis), on utilise toujours un modèle.

Chaque message.received contient windowExpiresAt, le moment où la fenêtre se ferme.

Un message libre hors fenêtre est accepté avec 202 puis échoue sur le webhook, avec le failureCode WINDOW_CLOSED. L'API ne le remplace pas d'elle-même par un modèle.

Envoyer un modèle

curl -X POST https://wa.automate-ai.digital/instances/ID_DU_NUMERO/messages/template \
  -H "x-api-key: $CLE" -H "content-type: application/json" \
  -d '{
    "to": { "kind": "contact", "id": "33612345678" },
    "templateName": "commande_confirmee",
    "languageCode": "fr",
    "bodyParameters": ["Marie", "A-12"],
    "buttonParameters": [{ "type": "url", "index": 1, "text": "A-12" }],
    "idempotencyKey": "commande-A-12-confirmee"
  }'

Champ

À quoi il sert

templateName

le nom du modèle, tel qu'il apparaît dans l'espace client

languageCode

la langue du modèle (fr, en, pt_PT…)

bodyParameters

les valeurs de {{1}}, {{2}}… du texte, dans l'ordre

headerParameter

la valeur de la variable de l'en-tête : du texte, ou une image { "kind": "url", "url": "…" }

buttonParameters

la valeur de chaque bouton de lien à partie variable

Bouton de lien à partie variable. Dans un modèle avec le lien https://boutique.fr/commande/{{1}}, chaque envoi doit indiquer ce qui remplace {{1}}. index est la position du bouton dans le modèle, en partant de 0 et en comptant tous les boutons (réponses rapides et téléphone compris). text est seulement la partie variable (A-12), jamais le lien entier. Sans cette valeur, Meta refuse l'envoi.

Créer des modèles

Dans l'espace client, sur le numéro officiel, onglet Modèles, cliquez sur Créer un modèle. Il y a quatre étapes : nom et catégorie, texte avec variables et exemples, boutons, et relecture. Meta examine chaque modèle, en général entre quelques minutes et quelques heures.

  • Utilitaire : avis sur quelque chose que le client a demandé (commande, rendez-vous, paiement). Approuvé plus vite.

  • Marketing : promotions et nouveautés.

Les modèles avec image en en-tête, carrousel ou authentification se créent dans le WhatsApp Manager de Meta. L'espace client propose un raccourci.

Quand Meta décide, vous recevez un événement template.status (voir Événements). Seul un modèle à l'état approved peut être envoyé.

Événements

Chaque événement arrive sur votre webhook en POST, avec un corps JSON. Il y a quatre types :

type

Quand

message.received

le client a écrit

message.status

un envoi a changé d'état : sent, held, delivered, read, failed

connection.updated

l'état du numéro a changé

template.status

un modèle a été approuvé, refusé, mis en pause…

Ignorez les types que vous ne connaissez pas, en répondant 2xx. La liste va s'allonger, et un flux qui suppose « si ce n'est pas A, c'est B » traite un nouveau type comme ce qu'il n'est pas.

message.received

{
  "type": "message.received",
  "eventId": "0f0f6ab7-2a2f-4f0b-9b6a-9a5f7d5d31c2",
  "instanceId": "0d9b1b6f-6a2a-4a4c-9f66-3f6a3f0b6a2a",
  "provider": "cloud_api",
  "occurredAt": "2026-11-12T18:41:07.000Z",
  "providerMessageId": "wamid.HBgM…",
  "from": "33612345678",
  "pushName": "Marie",
  "content": { "type": "text", "body": "Bonjour, où en est ma commande ?" },
  "windowExpiresAt": "2026-11-13T18:41:07.000Z"
}

Types de content :

type

Champs

text

body

media

mediaType (image, video, audio, document, sticker), storageUrl, storagePath, mimeType, filename, caption

location

latitude, longitude, name

interactive_reply

replyId, title : la réponse à un bouton

reaction

emoji, targetMessageId

unsupported

providerType : un format que l'API ne lit pas encore

Le storageUrl du média est temporaire : il sert à télécharger le fichier tout de suite, pas à le conserver.

Si le regroupement des messages est activé sur votre numéro, les textes consécutifs d'un même client arrivent en un seul événement, séparés par des retours à la ligne. Cet événement contient tous les identifiants dans providerMessageIds.

template.status

{
  "type": "template.status",
  "instanceId": "0d9b1b6f-…",
  "template": { "id": "806312974732579", "name": "commande_confirmee", "language": "fr", "category": "utility" },
  "status": "rejected",
  "reason": "invalid_format",
  "rejection": { "reason": "…", "recommendation": "…" }
}

status est en minuscules : pending, approved, rejected, paused, disabled, entre autres. Meta ajoute des états sans prévenir : traitez ceux qui vous intéressent et ignorez les autres. Un compte avec plusieurs numéros reçoit un avis par numéro ; dédupliquez par template.id avec status.

connection.updated

Contient status (connected, disconnected, logged_out…) et requiresReauth. Avec requiresReauth: true, le numéro doit être reconnecté dans l'espace client : l'autorisation de Meta n'est plus valable.

Vérifier la signature

Chaque requête du webhook contient :

x-timestamp: 1794519022
x-signature: sha256=9c1f…

La signature est un HMAC-SHA256, calculé avec le secret du webhook, sur x-timestamp + "." + corps brut. Pour la vérifier :

  1. refusez la requête s'il manque x-signature ou x-timestamp ;

  2. refusez-la si x-timestamp s'écarte de plus de 5 minutes de votre horloge ;

  3. calculez le HMAC sur le corps brut, octet par octet. Si vous resérialisez le JSON, la signature échoue pour certains événements ;

  4. comparez avec une comparaison à temps constant.

const crypto = require("crypto");

function signatureValide(secret, timestamp, corpsBrut, signature, signatureSuivante) {
  if (!signature || !timestamp) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const attendu = Buffer.from(
    "sha256=" + crypto.createHmac("sha256", secret).update(`${timestamp}.${corpsBrut}`).digest("hex")
  );
  const correspond = (h) => !!h && Buffer.from(h).length === attendu.length && crypto.timingSafeEqual(Buffer.from(h), attendu);
  return correspond(signature) || correspond(signatureSuivante);
}

Vérifiez avant de répondre. Si la signature échoue, répondez 401 : l'événement est conservé de notre côté et n'est pas perdu. Si vous répondez 2xx avant de vérifier, un événement refusé ensuite est perdu sans trace.

Changer le secret sans perdre d'événements. Pendant un changement, chaque requête contient aussi x-signature-next, signé avec le nouveau secret. Acceptez l'une ou l'autre signature. Vous pouvez ainsi passer au nouveau secret quand vous voulez, sans période où les événements échouent.

État des envois

status

Signification

sent

Meta a accepté le message

held

Meta a accepté le message et l'a retenu pour évaluer sa qualité. Ni succès ni échec pour l'instant

delivered

il est arrivé sur le téléphone du client

read

le client l'a lu

failed

il n'est pas parti : failureCode dit pourquoi et failureReason contient le texte de Meta

Avec held, attendez l'état suivant avant de confirmer quoi que ce soit au client.

Relier l'état au message

Les événements d'état contiennent messageId (l'identifiant que l'API vous a renvoyé), providerMessageId (l'identifiant de Meta), ou les deux. Jamais aucun des deux.

POST /messages            → { messageId: "4444…" }
message.status sent       → { messageId: "4444…", providerMessageId: "wamid.X" }   ← la paire
message.status delivered  → { providerMessageId: "wamid.X" }
message.status read       → { providerMessageId: "wamid.X" }

Conservez le providerMessageId reçu avec sent (ou held). Les delivered et read suivants ne contiennent que celui-là.

Erreurs

Dans la réponse à la requête. Elles se corrigent dans la requête elle-même :

HTTP

code

Quand

400

INVALID_PAYLOAD

le corps n'est pas correct (le message dit pourquoi)

401

UNAUTHORIZED

clé absente, erronée ou révoquée

404

INSTANCE_NOT_FOUND

le numéro n'existe pas dans votre compte

Sur le webhook, dans un message.status à failed. Tout ce qui dépend de Meta arrive ici, après le 202 :

failureCode

Quand

Où regarder

WINDOW_CLOSED

message libre hors de la fenêtre de 24 heures

envoyez un modèle

TEMPLATE_NOT_APPROVED

le modèle n'existe pas dans cette langue, n'est pas approuvé, ou a été mis en pause

onglet Modèles de l'espace client

INVALID_PAYLOAD

le nombre de variables ne correspond pas au modèle, ou le texte est trop long

le corps de la requête

CAPABILITY_UNAVAILABLE

quelque chose que ce canal ne fait pas (par exemple, un envoi à un groupe)

Limites du canal

INSTANCE_NOT_CONNECTED

le numéro n'est pas connecté

espace client

PROVIDER_REJECTED

Meta a refusé pour une autre raison

le texte de failureReason

Livraison des événements

  • Répondez 2xx dès réception, avant de traiter l'événement. Chaque tentative dispose de 10 secondes. Un endpoint qui ne répond qu'à la fin du flux risque de recevoir le même événement une deuxième fois.

  • Dédupliquez par eventId. Chaque événement est livré au moins une fois et peut arriver en double.

  • Triez par occurredAt, pas par ordre d'arrivée. Un delivered peut arriver avant le sent.

  • Nouvelles tentatives : sur un 5xx, un 429 ou sans réponse, nous réessayons jusqu'à 8 fois sur environ 30 minutes. Un 4xx (sauf 429) n'est pas réessayé : l'événement est conservé de notre côté.

Limites du canal

Le canal officiel n'a pas :

  • de groupes : il n'envoie ni ne reçoit de messages de groupe ;

  • d'indicateur « en train d'écrire », ni de suppression des messages envoyés ;

  • d'option note vocale : Meta décide selon le format du fichier (un audio ogg/opus s'affiche comme note vocale) ;

  • d'en-tête de modèle en base64 : utilisez une image par URL.

Ces requêtes échouent avec CAPABILITY_UNAVAILABLE, jamais en silence.

Coûts

  • L'abonnement (Officiel Essentiel, Business ou Scale) couvre les numéros et l'API, sans limite de messages de notre côté.

  • Les messages sont facturés par Meta, par message livré, sur votre compte WhatsApp Business. Le prix dépend de la catégorie (marketing, utilitaire, authentification) et du pays du client.

  • Les réponses dans la fenêtre de 24 heures sont des messages de service, gratuits jusqu'à la limite mensuelle fixée par Meta.

  • Sans moyen de paiement sur le compte Meta, Meta cesse d'envoyer les messages, y compris les réponses dans la fenêtre.

Les prix en vigueur figurent sur la page de tarification de la plateforme WhatsApp de Meta.