Sign inGet Started

Envoyer des messages WhatsApp

Ce guide couvre le point de terminaison d'envoi, POST /v1/whatsapp/messages. Vous construisez un payload JSON avec un destinataire et exactement un type de contenu : un template pré-approuvé, ou un message de service contenant du texte, une image, une vidéo, un audio, un sticker, un document, une localisation, des fiches contact ou un élément interactif. Bird renvoie 202 Accepted avec un ID de message et effectue la livraison de manière asynchrone. Le type que vous pouvez envoyer dépend de la fenêtre de service client. Chaque requête envoie un message à un destinataire, et il n'existe pas de point de terminaison par lot.

Un envoi minimal

Le plus petit payload valide est un destinataire to et un template avec son slug. Ajoutez language si vous voulez une langue spécifique ; l'omettre envoie la langue par défaut du template, et remplissez les variables déclarées par le template via components.
L'appel curl nomme l'hôte US ; si votre clé commence par bk_eu1_, appelez https://eu1.platform.bird.com à la place. Les SDK détectent la région à partir de votre clé, ils ne définissent donc aucun hôte.
const msg = await bird.whatsapp.send({
  to: "+15551234567",
  template: {
    slug: "bird_otp",
    components: [{ type: "body", parameters: [{ type: "text", text: "123456" }] }],
  },
});
console.log(msg.id, msg.status);

La fenêtre de service client

Le type que vous pouvez envoyer dépend d'un seul état : si la fenêtre de service client est ouverte ou non.
Le contact ouvre la fenêtre en envoyant un message ou en appelant votre numéro professionnel, et elle reste ouverte pendant 24 heures, se réinitialisant chaque fois qu'il vous envoie un nouveau message. Tant qu'elle est ouverte, vous pouvez envoyer un message de service, c'est-à-dire tout contenu libre : texte, image, vidéo, audio, sticker, document, localisation ou interactif. Une fois qu'elle expire, seul un template pré-approuvé peut l'atteindre, et sa réponse à ce template rouvre la fenêtre.
Bird suit la fenêtre pour vous, de sorte qu'un message de service envoyé dans une fenêtre fermée est refusé avant que quoi que ce soit ne soit créé ou facturé : la requête renvoie un 422 E15044 WhatsAppServiceWindowClosed. La vérification fonctionne au mieux et échoue de manière permissive, donc un 202 ne prouve pas que la fenêtre était réellement ouverte au moment de l'envoi ; une fenêtre qui expire entre l'acceptation et l'envoi échoue de manière asynchrone, avec service_window_expired sur le last_error du message.
Consultez la fenêtre de service client pour le cycle de vie complet : ce qui l'ouvre, ce qui la réinitialise et comment elle interagit avec la tarification.

Construire le payload

Destinataire

to est un destinataire unique, donné soit comme numéro de téléphone, soit comme identifiant utilisateur limité au périmètre business. Un numéro de téléphone est au format E.164 : un + initial, l'indicatif pays et le numéro d'abonné, par exemple +14155550100. Nous validons le numéro, donc une valeur qui ne peut pas être un vrai numéro composable (longueur incorrecte, préfixe non attribué) est rejetée avec un 422 WhatsAppInvalidRecipient avant toute facturation. Un message va à un seul destinataire ; il n'y a ni tableau de destinataires ni envoi par lot, donc pour atteindre plusieurs personnes, effectuez un appel par destinataire.
Un identifiant utilisateur limité au périmètre business tel que US.13491208655302741918 s'adresse à un contact dont vous n'avez pas le numéro de téléphone, ce qui vous permet de répondre à un contact qui vous a joint sans en fournir. Deux choses changent : le numéro d'envoi doit appartenir au même portefeuille business auquel l'identifiant est rattaché, et un template de code à usage unique nécessite un numéro de téléphone. Un tel template de code à usage unique géré par Bird est refusé à l'acceptation avec un 422 WhatsAppRecipientNotSupportedForTemplate ; un template d'authentification créé par votre espace de travail est accepté puis échoue, car Meta exige un numéro de téléphone pour celui-ci.

Template

template nomme le template pré-approuvé à envoyer :
  • slug (obligatoire) : le slug du template, par exemple bird_order_confirmation. Il doit correspondre à un template de votre catalogue (lettres minuscules, chiffres et underscores).
  • language : le tag de langue du template, par exemple en ou pt-BR. Omettez-le pour envoyer la langue par défaut du template ; nommer une langue que le template ne possède pas renvoie un 422 qui liste celles disponibles. Le message accepté renvoie la langue résolue.
  • components : les valeurs qui remplissent les variables du template (voir Composants et paramètres). Omettez-le pour un template sans variables.
Parcourez vos templates, leurs langues et un aperçu rendu de chacun sur la page Templates.

Composants et paramètres

Les templates contiennent des variables, nommées ({{ref}}, {{amount}}) ou numérotées ({{1}}, {{2}}). Vous fournissez leurs valeurs via components. Chaque composant nomme un type (body ou button) et un tableau parameters. Chaque paramètre nomme son propre type (text, image, video, gif, document ou location) et porte le champ correspondant : text une chaîne simple, image/video/gif/document une url https publique, et location un point sur la carte. Un template avec des paramètres nommés exige un name sur chaque paramètre, correspondant exactement aux noms déclarés par le template (voir Référence des champs). Un template positionnel omet name et prend ses valeurs dans l'ordre {{n}}, de sorte que le premier paramètre remplit {{1}}. Dans les deux cas, des paramètres qui ne correspondent pas à ce que le template déclare renvoient un 422 WhatsAppTemplateParameterMismatch. Un type de composant header existe aussi dans le format d'échange : sur un template géré par Bird, il est ignoré, car aucun template géré par Bird ne déclare de variable d'en-tête, mais sur un template créé par votre espace de travail, il est transmis, ce qui permet à un template utilitaire ou marketing avec en-tête média de recevoir son image.
Par exemple, un template de code à usage unique dont le corps indique {{1}} is your verification code et dont le bouton copie le code prend le code à la fois comme paramètre de corps et comme paramètre de bouton, de manière positionnelle (sans name) :
Exemple de code
{
  "components": [
    { "type": "body", "parameters": [{ "type": "text", "text": "481920" }] },
    { "type": "button", "parameters": [{ "type": "text", "text": "481920" }] }
  ]
}

Catégorie et expéditeur

La catégorie d'un template (authentication, utility ou marketing) détermine comment WhatsApp traite le message et, combinée au pays de destination, son coût.
Le propriétaire de l'expéditeur détermine si vous devez le nommer :
  • Un template géré par Bird (son slug commence par bird_) envoie depuis le numéro que Bird conserve pour cette catégorie, donc omettez from. Le définir renvoie un 422 WhatsAppSenderNotAllowed.
  • Tout le reste nomme son propre expéditeur dans from : un message de service de tout type, et tout template créé par votre espace de travail. Le numéro doit appartenir à votre espace de travail. L'omettre renvoie un 422 WhatsAppSenderRequired, et un numéro depuis lequel l'espace de travail ne peut pas envoyer renvoie un 422 WhatsAppSenderNotFound. Un template créé par vous doit aussi être rattaché au même WhatsApp Business Account que le numéro, sinon l'envoi renvoie un 422 WhatsAppSenderWABAMismatch.
Configuration du numéro de téléphone couvre les deux types de numéro et la façon dont un numéro qui vous appartient est connecté.

Messages de service

Au lieu de template, incluez exactement un parmi text, image, video, audio, sticker, document, location, contact_cards ou interactive. Tous les neuf sont des messages de service, ils nécessitent donc une fenêtre de service client ouverte. Chacun d'eux exige aussi from, un numéro appartenant à votre espace de travail ; les numéros gérés par Bird ne peuvent pas le porter.
  • text : { "body": "..." }, jusqu'à 4 096 caractères. Ajoutez "preview_url": true pour afficher un aperçu de lien pour la première URL dans body.
  • image, video, audio, sticker, document : chacun prend une URL https publique que WhatsApp récupère au moment de l'envoi (url), donc une URL signée doit rester valide au-delà de l'envoi. Une URL http est rejetée directement. WhatsApp récupère le fichier lui-même, donc une URL qu'il ne peut pas atteindre, une URL servant un type non pris en charge ou un fichier dépassant la limite de taille pour son type est accepté puis échoue, avec media_rejected sur le last_error du message et la raison propre de WhatsApp dans description. image, video et document acceptent aussi un caption optionnel ; document accepte aussi un filename optionnel ; audio accepte un flag voice optionnel pour un rendu en note vocale.
  • location : { "latitude": ..., "longitude": ... } (tous deux obligatoires, en degrés décimaux) plus name et address optionnels.
  • contact_cards : un tableau de jusqu'à cinq contacts partagés dans un seul message. Le name de chaque fiche nécessite formatted_name plus au moins une autre partie (first_name, last_name, middle_name, prefix ou suffix) ; phone_numbers, emails, urls et addresses acceptent chacun jusqu'à dix entrées, et org et birthday (sous forme de YYYY-MM-DD) sont optionnels. Un phone_number au format E.164 donne à cette fiche un bouton qui ouvre une conversation avec ce contact.
  • interactive : du texte de corps plus un élément interactif, dans l'un des six types : boutons de réponse, menu de liste, bouton de lien, carrousel média, ou un bouton unique demandant au destinataire sa localisation ou son numéro de téléphone. Messages interactifs couvre le format d'échange de chaque type, les réponses produites par un appui et les limites.
Exemple de code
{
  "to": "+16505551234",
  "from": "+13124495648",
  "text": { "body": "Your order shipped: https://example.com/track/A1B2C3", "preview_url": true }
}
Une requête ne contenant aucun contenu, ou plus d'un type, est rejetée avec un 422.

Citer un message

Définissez in_reply_to_message_id sur un ID de message WhatsApp pour envoyer votre message comme réponse à celui-ci, de la même manière qu'appuyer sur répondre dans l'application WhatsApp cite un message. Le destinataire voit votre message avec le message cité au-dessus, et le champ est renvoyé à chaque lecture du message.
Cela fonctionne aussi dans l'autre sens : un message entrant que WhatsApp marque comme réponse porte l'ID du message cité dans le même champ, ce qui vous permet de savoir auquel de vos messages une réponse correspond. Un message entrant que WhatsApp ne marque pas ne porte aucun ID, et la résolution peut aussi échouer. Pour une corrélation fiable, utilisez des identifiants de réponse interactive explicites avec l'état de conversation ou de tâche stocké dans votre application. Le metadata sortant reste sur l'enregistrement sortant et n'est pas automatiquement copié sur la réponse.
La citation est résolue avant l'acceptation de l'envoi, donc une citation qui ne peut pas être rendue fait échouer la requête elle-même et rien n'est créé ni facturé. Un id ne désignant aucun message détenu par cet espace de travail, ou un message datant de plus de 15 jours (durée pendant laquelle un message reste citable), renvoie un 404 E15071 WhatsAppReferencedMessageNotFound. Un id désignant un message qui n'a jamais atteint WhatsApp, ou un message d'une conversation différente de celle du to et du from de cet envoi, renvoie un 422 E15072 WhatsAppMessageNotQuotable. Si Bird ne peut pas atteindre le stockage permettant de résoudre la référence, l'envoi renvoie un 503 E15073 WhatsAppMessageLookupUnavailable, qu'il vaut la peine de réessayer. La citation fonctionne aussi bien sur un envoi de template que sur un envoi libre.
Exemple de code
{
  "to": "+16505551234",
  "from": "+13124495648",
  "in_reply_to_message_id": "wam_01kya19eknftrs2s6p82asmvnh",
  "text": { "body": "Yes, that slot is still free." }
}

Tags et métadonnées

Deux champs optionnels attachent votre propre contexte à un message ; tous deux sont renvoyés lors des lectures API et accompagnent chaque événement webhook du message :
  • tags : jusqu'à 20 libellés { "name": ..., "value": ... } structurés pour des dimensions à faible cardinalité sur lesquelles vous filtrez et produisez des rapports (une campagne, une variante d'expérience). Les noms et valeurs acceptent les lettres ASCII, les chiffres, l'underscore et le tiret ; les noms sont limités à 32 caractères et uniques au sein d'un envoi, les valeurs à 64. Filtrez la liste de messages par tag (?tag=campaign ou ?tag=campaign:launch-week), et la page Metrics ventile la livraison par tag.
  • metadata : un objet JSON arbitraire, jusqu'à 2 Ko sérialisé, pour un contexte par envoi dont vous n'avez pas besoin comme dimension de filtrage (un ID de commande interne, une référence de session).
Exemple de code
{
  "tags": [{ "name": "campaign", "value": "order-confirmations" }],
  "metadata": { "order_id": "ord_8271" }
}

Référence des champs

ChampTypeObligatoireLimites / notes
tostringouiUn destinataire par message : un numéro de téléphone E.164 ou un identifiant utilisateur limité au périmètre business, qu'aucun template de code à usage unique n'accepte
fromstring (E.164)non**Omettez pour un template géré par Bird, qui choisit son propre expéditeur ; obligatoire pour un message de service et pour un template créé par votre espace de travail, et doit être un numéro appartenant à votre espace de travail
template.slugstringnon**Un slug de template que votre espace de travail peut envoyer ; les slugs gérés par Bird commencent par bird_
template.languagestringnon*Tag de langue du template (en, pt-BR) ; omettez pour envoyer la langue par défaut du template
template.componentsarraynonRemplit les variables du template ; le type du composant est body ou button
template.components[].parameters[].namestringnon†Le placeholder que cette valeur remplit, par exemple ref ; obligatoire et doit correspondre aux noms déclarés du template pour un template à paramètres nommés, omis pour un template positionnel
interactiveobjectnon**Texte de corps plus un type de contenu interactif ; un message de service, nécessite donc une fenêtre de service ouverte. Voir Messages interactifs
in_reply_to_message_idstringnonUn ID de message WhatsApp détenu par cet espace de travail, cité dans le message que vous envoyez ; renvoyé lors des lectures. Voir Citer un message
tagsarraynonJusqu'à 20 libellés {name, value} ; nom ≤ 32 car., valeur ≤ 64, noms uniques
metadataobjectnonJSON arbitraire, jusqu'à 2 Ko sérialisé
* language est optionnel ; l'omettre envoie la langue par défaut du template. † name est obligatoire sur chaque paramètre pour un template à paramètres nommés. Omettez-le pour un template positionnel. Voir Composants et paramètres. ** Incluez exactement un parmi template ou un champ de contenu de message de service (text, image, video, audio, sticker, document, location, interactive) ; voir Messages de service.

Le modèle asynchrone : ce que signifie 202

Un envoi réussi renvoie 202 Accepted avec un ID de message et status: accepted. Le 202 n'est renvoyé qu'après l'acceptation durable de l'envoi ; il n'est jamais accepté puis silencieusement abandonné. Les erreurs fatales que vous pouvez corriger échouent immédiatement avec un 422 : un destinataire invalide, un slug ou une langue de template inconnus, une incompatibilité de paramètres, ou un message de service envoyé dans une fenêtre de service client fermée (WhatsAppServiceWindowClosed). Un portefeuille non approvisionné n'en fait pas partie : l'envoi est accepté, et le message termine rejected avec insufficient_balance une fois que Bird tente de le facturer. La livraison effective se fait de manière asynchrone : le message passe à sent quand nous le transmettons à WhatsApp, puis à un statut terminal (delivered ou failed) quand l'accusé de réception arrive, signalé via les événements, les webhooks et les points de terminaison de lecture. Un accusé de lecture est exposé séparément sous forme d'un horodatage read_at et d'un événement whatsapp.read plutôt que comme un statut.
Note de confidentialité : pour les templates de catégorie authentication, l'API ne renvoie jamais les valeurs remplies. L'écho 202 et chaque lecture ultérieure portent un tableau components vide pour ces messages, de sorte qu'un code de vérification ne réapparaît jamais.

Réessayer en toute sécurité

Envoyez l'en-tête Idempotency-Key avec une valeur unique par envoi logique, et les réessais deviennent sûrs. Si votre première requête a réussi mais que vous n'avez jamais vu la réponse (délai d'attente, connexion interrompue), rejouer la requête avec la même clé renvoie le résultat original au lieu d'envoyer, et de facturer, un message en double. La réponse rejouée porte un en-tête Idempotency-Replay. Voir idempotence pour le format de clé et la rétention.

Recevoir la réponse

Les messages entrants arrivent sur la même ressource que les messages sortants, et chacun d'eux réinitialise la fenêtre de service. Recevoir des messages WhatsApp couvre leur lecture via l'API, la récupération du média envoyé par un contact et le webhook whatsapp.received.

Coût et facturation

WhatsApp est facturé par message, en fonction de la catégorie du template et du pays du destinataire ; voir Tarification WhatsApp. Un message est facturé en deux étapes, à deux moments différents, et l'objet cost du message rend compte des deux :
ChampDescriptionMoment d'apparition
transaction_amountFrais de Bird pour le traitement de l'envoiQuand Bird traite l'envoi accepté, avant l'expédition
passthrough_amountPart de Meta dans le prix du message, que Bird répercuteQuand un accusé de réception delivered ou read applicable arrive
amountLa somme des composants tarifés jusqu'à présentAugmente à mesure que chaque composant arrive
currency_codeLa devise du portefeuille de votre organisation, commune aux deux composantsAvec le premier composant
Les deux montants sont des chaînes décimales, hors taxes.
Les deux composants sont tarifés sur des bases différentes. Les frais de Bird utilisent la catégorie du template que vous avez envoyé et le pays du destinataire, qui provient de l'indicatif pays du numéro de téléphone ou, pour un envoi adressé à un identifiant utilisateur limité au périmètre business, du préfixe à deux lettres de cet identifiant. La part de Meta utilise la catégorie que Meta lui-même signale sur l'accusé de réception applicable, qui peut différer de celle du template : Meta peut signaler authentication-international quand ses règles de destination, de localisation de l'entreprise et d'éligibilité s'appliquent. Voir Tarifs authentication-international WhatsApp.
Ce que cost affiche dépend de l'avancement du message :
  • Au moment du 202, cost est null. Rien n'a été tarifé.
  • Après le traitement, transaction_amount est défini et amount lui est égal. passthrough_amount reste null.
  • Après un accusé de réception delivered ou read applicable, des frais Meta enregistrés avec succès alimentent passthrough_amount, et amount reflète les composants enregistrés.
Un composant null signifie qu'aucun montant n'est enregistré dans cette projection ; ce n'est pas la preuve que le message était gratuit. Un composant explicitement tarifé à zéro affiche "0.00000".
Les deux prélèvements échouent aussi différemment. Les frais de Bird échouent de manière restrictive : quand ils ne peuvent pas aboutir après le 202 parce que le portefeuille ne peut pas couvrir l'envoi ou que la route n'a pas de prix configuré, le message passe au statut rejected avec le code d'erreur insufficient_balance ou price_not_found, et rien n'est facturé. Un message rejected n'a jamais atteint WhatsApp, ce qui le distingue de failed. La part de Meta échoue de manière permissive : si le portefeuille est insuffisant ou que le tarif est manquant quand l'accusé de réception arrive, le prélèvement est ignoré et l'état observé du message est conservé. Votre livraison n'est jamais retardée par le second prélèvement.
Un message facturé par Bird conserve ces frais sortants même si la livraison échoue ensuite. Les frais Meta sont traités à partir d'un callback delivered ou read applicable quand Meta signale une tarification régulière avec une catégorie et une destination résolvables. Les deux chemins de callback utilisent la même identité de frais et s'appuient sur la déduplication du service de facturation. Réconciliez les accusés de réception rejoués avec les enregistrements de facturation au lieu de traiter la projection du message comme un reçu de débit permanent. La tarification de service ou d'entrée gratuite peut rendre le composant Meta égal à zéro ; un composant non résolu ne prouve pas que le message était gratuit.
Utilisez le grand livre de facturation pour la réconciliation financière. Les champs cost du message sont des projections des frais et peuvent être en retard ou rester incomplets. Voir Métriques WhatsApp pour la distinction entre observations de messages et enregistrements de facturation.
Les événements WhatsApp ne contiennent pas le coût. Pour lire l'un ou l'autre composant, relisez le message avec GET /v1/whatsapp/messages/{id}.

Étapes suivantes