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);msg = client.whatsapp.send(
to="+31612345678",
template="bird_otp",
language="en",
components=[{"type": "body", "parameters": [{"type": "text", "text": "123456"}]}],
)
print(msg.id, msg.status)code := "123456"
msg, err := client.Whatsapp.Send(context.Background(), bird.WhatsappSendParams{
To: "+15551234567",
Template: "bird_otp",
Language: "en",
Components: []bird.WhatsAppMessageTemplateComponent{{
Type: "body",
Parameters: &[]bird.WhatsAppMessageTemplateComponentParameter{{Type: "text", Text: &code}},
}},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.Id, *msg.Status)$message = $bird->whatsapp->send(
to: '+15551234567',
template: 'bird_otp',
language: 'en',
components: [
(new WhatsAppMessageTemplateComponent())
->setType('body')
->setParameters([
(new WhatsAppMessageTemplateComponentParameter())->setType('text')->setText('123456'),
]),
],
);
echo $message->getId(), ' ', $message->getStatus();bird whatsapp send \
--components '[{"parameters":[{"text":"1234","type":"text"}],"type":"body"},{"parameters":[{"text":"1234","type":"text"}],"type":"button"}]' \
--language en \
--template bird_otp \
--to +31612345678curl -X POST "https://{region}.platform.bird.com/v1/whatsapp/messages" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to": "+31612345678",
"template": {
"slug": "bird_otp",
"language": "en",
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "1234"
}
]
},
{
"type": "button",
"parameters": [
{
"type": "text",
"text": "1234"
}
]
}
]
}
}'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 désigne une seule destination, sous forme de numéro de téléphone, d'identifiant utilisateur à portée business ou d'identifiant de groupe. Un numéro de téléphone est au format E.164 : un + en tête, l'indicatif pays et le numéro d'abonné, par exemple +14155550100. Le numéro est validé : une valeur qui ne peut pas être un numéro réel et composable (longueur incorrecte, préfixe non attribué) est rejetée avec un 422 WhatsAppInvalidRecipient avant toute facturation. Il n'y a pas de tableau de destinataires ni d'envoi par lot : pour joindre plusieurs personnes qui ne font pas partie d'un même groupe, il faut 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.
to accepte une forme supplémentaire : un identifiant de groupe WhatsApp tel que wag_01krdgeqcxet5s7t44vh8rt9mg, qui envoie le message à chaque participant de cette conversation de groupe. Un envoi de groupe omet from et rend compte de la distribution à l'échelle du groupe plutôt que pour un seul destinataire ; Envoyer à un groupe WhatsApp traite le sujet sur sa propre page.
Template
template désigne 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 en écho 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 indique un type (body ou button) et un tableau parameters. Chaque paramètre indique son propre type (text, image, video, gif, document ou location) et porte le champ correspondant : text une chaîne de texte, image/video/gif/document une https url publique, et location un point sur la carte. Un template à 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 ne correspondant pas à ce que le template déclare renvoient un 422 WhatsAppTemplateParameterMismatch. Un type de composant header existe aussi sur le fil : 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 ; 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 affiche {{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 façon 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 la façon dont 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_) est envoyé depuis le numéro que Bird conserve pour cette catégorie ; omettez donc 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éé doit aussi se trouver sur le même WhatsApp Business Account que le numéro, sans quoi 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 à vous est connecté.
Messages de service
Au lieu de template, fournissez exactement l'un des champs suivants : text, image, video, audio, sticker, document, location, contact_cards ou interactive. Les neuf sont des messages de service et nécessitent donc une fenêtre de service client ouverte. Chacun 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) ; une URL signée doit donc rester valide au-delà de l'envoi. Une URL http est rejetée immédiatement. WhatsApp récupère le fichier lui-même ; une URL inaccessible, servant un type non pris en charge ou un fichier dépassant la limite de taille pour son type est acceptée puis échoue, avec media_rejected sur le last_error du message et la raison propre à 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": ... } (les deux obligatoires, en degrés décimaux) plus name et address optionnels.
- contact_cards : un tableau de cinq contacts maximum 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 ajoute à cette fiche un bouton ouvrant une conversation avec ce contact.
- interactive : du texte dans le corps plus un élément interactif, selon l'un des six types : boutons de réponse, menu à 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 la structure de chaque type, les réponses générées 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 avec un identifiant de message WhatsApp pour envoyer votre message en réponse à celui-ci, comme lorsqu'on appuie sur répondre dans l'application WhatsApp pour citer 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'identifiant du message cité dans le même champ, ce qui vous permet de savoir auquel de vos messages la réponse correspond. Un message entrant que WhatsApp ne marque pas ne porte aucun identifiant, 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é par 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 que l'envoi ne soit accepté : une citation qui ne peut pas être rendue fait échouer la requête elle-même, et rien n'est créé ni facturé. Un identifiant 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 identifiant désignant un message qui n'a jamais atteint WhatsApp, ou un message d'une conversation différente de celle définie par to et from de cet envoi, renvoie un 422 E15072 WhatsAppMessageNotQuotable. Si Bird ne peut pas joindre le store qui répond à la question, 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 ; les deux sont renvoyés lors des lectures API et accompagnent chaque événement webhook du message :
- tags : jusqu'à 20 labels { "name": ..., "value": ... } structurés pour des dimensions à faible cardinalité sur lesquelles vous filtrez et créez des rapports (une campagne, une variante d'expérience). Les noms et valeurs acceptent les lettres ASCII, chiffres, underscores et tirets ; les noms sont limités à 32 caractères et uniques par envoi, les valeurs à 64. Filtrez la liste des messages par tag (?tag=campaign ou ?tag=campaign:launch-week), et la page Métriques ventile la distribution par tag.
- metadata : un objet JSON arbitraire, jusqu'à 2 Ko sérialisé, pour du contexte par envoi dont vous n'avez pas besoin comme dimension de filtre (un identifiant 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
| Champ | Type | Obligatoire | Limites / notes |
|---|---|---|---|
| to | string | oui | Un destinataire par message : un numéro de téléphone E.164, un identifiant utilisateur à portée business, qu'aucun template de code à usage unique n'accepte, ou un identifiant de groupe WhatsApp (wag_…), qui envoie à chaque participant du groupe |
| from | string (E.164) | non** | Omettez pour un template géré par Bird, qui choisit son propre expéditeur, et pour un envoi de groupe, qui utilise le numéro du groupe ; 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.slug | string | non** | Un slug de template que votre espace de travail peut envoyer ; les slugs gérés par Bird commencent par bird_ |
| template.language | string | non* | Tag de langue du template (en, pt-BR) ; omettez pour envoyer la langue par défaut du template |
| template.components | array | non | Remplit les variables du template ; le type du composant est body ou button |
| template.components[].parameters[].name | string | non† | 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 |
| interactive | object | non** | Texte du corps plus un type de contenu interactif ; message de service, nécessite donc une fenêtre de service ouverte. Voir Messages interactifs |
| in_reply_to_message_id | string | non | Un identifiant de message WhatsApp détenu par cet espace de travail, cité dans le message que vous envoyez ; renvoyé en écho à la lecture. Voir Citer un message |
| tags | array | non | Jusqu'à 20 labels {name, value} ; nom ≤ 32 car., valeur ≤ 64, noms uniques |
| metadata | object | non | JSON 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.
** Fournissez exactement l'un des champs suivants : 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 identifiant de message et status: accepted. Le 202 n'est renvoyé qu'une fois l'envoi durablement accepté ; il n'est jamais accepté puis silencieusement abandonné. Les erreurs définitives que vous pouvez corriger échouent immédiatement avec un 422 : destinataire invalide, slug ou langue de template inconnu, incohérence de paramètres, ou 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 se termine rejected avec insufficient_balance lorsque Bird tente de le facturer. La distribution effective se fait de façon asynchrone : le message passe à sent lorsque nous le transmettons à WhatsApp, puis à un statut terminal (delivered ou failed) à l'arrivée de l'accusé de réception, signalé via les événements, les webhooks et les endpoints 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, et non comme un statut.
Note de confidentialité : pour les templates de catégorie authentication, le API ne renvoie jamais les valeurs remplies. L'écho du 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 nouvelles tentatives deviennent sûres. Si votre première requête a réussi mais que vous n'avez jamais vu la réponse (timeout, connexion interrompue), la rejouer 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 durée de rétention.
Recevoir la réponse
Les messages entrants arrivent sur la même ressource que les messages sortants, et chacun d'entre eux réinitialise la fenêtre de service. Recevoir des messages WhatsApp couvre leur lecture via le 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 distincts, et l'objet cost du message rend compte des deux :
| Champ | Description | Moment d'apparition |
|---|---|---|
| transaction_amount | Frais de Bird pour le traitement de l'envoi | Lorsque Bird traite l'envoi accepté, avant la distribution |
| passthrough_amount | Part de Meta dans le prix du message, que Bird répercute | À l'arrivée d'un accusé delivered ou read applicable |
| amount | La somme des composants tarifés jusqu'ici | Augmente à mesure que chaque composant arrive |
| currency_code | La devise du portefeuille de votre organisation, commune aux deux composants | Avec le premier composant |
Les deux montants sont des chaînes décimales, hors taxes.
Note: a service message carries no Meta share until September 30th 2026, and neither does a utility template delivered inside an open customer service window. From October 1st 2026 Meta charges for both, except inside a 72-hour free entry point window, and with 1,000 free service messages per business phone number per month: October 2026 pricing changes.
Les deux composants sont tarifés sur des données différentes. Les frais de Bird utilisent la catégorie du template que vous avez envoyé et le pays du destinataire, issu de l'indicatif pays du numéro de téléphone ou, pour un envoi adressé à un identifiant utilisateur à portée 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é applicable, qui peut différer de celle du template : Meta peut signaler authentication-international lorsque 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é delivered ou read applicable, un frais Meta enregistré avec succès remplit 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 frais échouent aussi de manière différente. Les frais de Bird échouent de façon fermée : lorsqu'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 se termine 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 façon ouverte : si le portefeuille est insuffisant ou si le tarif est manquant à l'arrivée de l'accusé, le frais est ignoré sans annuler l'état observé du message. Votre distribution n'est jamais retardée par le second frais.
Un message facturé par Bird conserve ce frais sortant même si la distribution échoue par la suite. Les frais Meta sont traités à partir d'un callback delivered ou read applicable lorsque Meta signale une tarification standard 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 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 amener le composant Meta à zéro ; un composant non résolu n'est pas la preuve 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 portent pas de coût. Pour lire l'un ou l'autre composant, relisez le message avec GET /v1/whatsapp/messages/{id}.
Étapes suivantes
- Messages de service : les neuf types de contenu libre et ce que chacun accepte
- Recevoir des messages WhatsApp : messages entrants, médias et webhook whatsapp.received
- Identifiants utilisateur à portée business : adresser un contact qui vous a joint sans numéro de téléphone
- Templates : parcourir le catalogue et lire les variables d'un template
- Envoyer à un groupe WhatsApp : adresser un groupe et lire ses accusés par participant
- Idempotence : nouvelles tentatives sûres avec l'en-tête Idempotency-Key
Ressources associées
Poursuivez avec la documentation, les guides et les exemples sur ce sujet. Les ressources sont en anglais.
Regarder le guideConnecting WhatsApp to Bird: from buying a number to a live channelComprendre le conceptWhat is the 24-hour customer service window on WhatsApp?Utiliser l'outilWhatsApp message builderExplorer la fonctionnalitéWhatsApp
Essayez la pratique et obtenez un guide d'implémentation