Sign inGet Started

WhatsApp-berichten versturen

Deze handleiding behandelt het verstuurendpoint POST /v1/whatsapp/messages. Je bouwt één JSON-payload met een ontvanger en precies één type inhoud: een goedgekeurde template, of een servicebericht met tekst, een afbeelding, video, audio, een sticker, een document, een locatie, contactkaarten of iets om op te tikken. Bird retourneert 202 Accepted met een bericht-ID en levert asynchroon af. Welke van de twee je kunt versturen hangt af van het klantenservicevenster. Elk request stuurt één bericht naar één ontvanger, en er is geen batch-endpoint.

Een minimale verzending

De kleinste geldige payload is een to-ontvanger en een template met zijn slug. Voeg language toe als je een specifieke taal wilt; weglaten stuurt de standaardtaal van de template, en vul eventuele variabelen die de template declareert via components.
De curl-aanroep noemt de US-host; als je key begint met bk_eu1_, gebruik dan https://eu1.platform.bird.com. De SDK's lezen de regio uit je key en stellen geen host in.
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);

Het klantenservicevenster

Welke van de twee je kunt versturen hangt af van één stuk state: of het klantenservicevenster open is.
Het contact opent het venster door je zakelijke nummer te berichten of te bellen, en het blijft 24 uur open, waarbij het elke keer wordt gereset als ze je opnieuw berichten. Zolang het open is kun je een servicebericht sturen, dus elke vorm van vrij geformuleerde inhoud: tekst, afbeelding, video, audio, sticker, document, locatie of interactief. Zodra het vervalt, bereikt alleen een goedgekeurde template hen, en hun antwoord daarop heropent het venster.
Bird houdt het venster voor je bij, dus een servicebericht naar een gesloten venster wordt geweigerd voordat er iets wordt aangemaakt of in rekening gebracht: het request retourneert een 422 E15044 WhatsAppServiceWindowClosed. De controle is best effort en faalt open, dus een 202 is geen bewijs dat het venster daadwerkelijk open was bij verzending; een venster dat vervalt tussen accepteren en verzenden faalt asynchroon, met service_window_expired op de last_error van het bericht.
Zie het klantenservicevenster voor de volledige levenscyclus: wat het opent, wat het reset, en hoe het samenhangt met prijsstelling.

De payload opbouwen

Ontvanger

to benoemt één bestemming, opgegeven als telefoonnummer, business-scoped user ID of groeps-ID. Een telefoonnummer is in E.164-formaat: een + vooraan, landcode en abonneenummer, zoals +14155550100. We valideren het nummer, dus een waarde die geen echt, belbaar nummer kan zijn (verkeerde lengte, niet-toegewezen prefix) wordt geweigerd met een 422 WhatsAppInvalidRecipient voordat er iets in rekening wordt gebracht. Er is geen ontvanger-array en geen batchverzending, dus veel mensen bereiken die niet in één groep zitten betekent één aanroep per ontvanger.
Een business-scoped user ID zoals US.13491208655302741918 adresseert een contact van wie je het telefoonnummer niet hebt, en zo beantwoord je een contact dat je zonder telefoonnummer heeft bereikt. Twee dingen veranderen: het verzendnummer moet tot hetzelfde bedrijfsportfolio behoren waartoe het ID is gescoped, en een eenmalige-verificatiecodetemplate heeft een telefoonnummer nodig. Een door Bird beheerde wordt geweigerd bij acceptatie met een 422 WhatsAppRecipientNotSupportedForTemplate; een authenticatietemplate die je werkruimte heeft gemaakt wordt geaccepteerd en faalt daarna, omdat Meta er een telefoonnummer voor vereist.
to heeft nog een vorm: een WhatsApp groeps-ID zoals wag_01krdgeqcxet5s7t44vh8rt9mg, die naar elke deelnemer van die groepschat stuurt. Een groepsverzending laat from weg en rapporteert de bezorging over de groep in plaats van per ontvanger, dus Verzenden naar een WhatsApp-groep behandelt dat op een eigen pagina.

Template

template benoemt het vooraf goedgekeurde template dat je verstuurt:
  • slug (verplicht): de slug van het template, zoals bird_order_confirmation. Het moet overeenkomen met een template in je catalogus (kleine letters, cijfers en underscores).
  • language: de taaltag van het template, zoals en of pt-BR. Laat het weg om de standaardtaal van het template te verzenden; een taal opgeven die het template niet heeft geeft een 422 terug met de talen die het wél heeft. Het geaccepteerde bericht echoot de bepaalde taal.
  • components: de waarden die de variabelen van het template invullen (zie Componenten en parameters). Laat het weg voor een template zonder variabelen.
Blader door je templates, hun talen en een gerenderde preview van elk op de pagina Templates.

Componenten en parameters

Templates bevatten variabelen, benoemd ({{ref}}, {{amount}}) of genummerd ({{1}}, {{2}}). Je levert hun waarden aan via components. Elk component benoemt een type (body of button) en een parameters-array. Elke parameter benoemt zijn eigen type (text, image, video, gif, document of location) en bevat het bijbehorende veld: text een platte string, image/video/gif/document een publieke https url, en location een punt op de kaart. Een template met benoemde parameters vereist een name op elke parameter, die exact overeenkomt met de namen die het template declareert (zie Veldreferentie). Een positioneel template laat name weg en neemt zijn waarden in {{n}}-volgorde, zodat de eerste parameter {{1}} vult. In beide gevallen geven parameters die niet overeenkomen met wat het template declareert een 422 WhatsAppTemplateParameterMismatch terug. Een header-componenttype bestaat ook op de wire: bij een Bird-beheerd template wordt het weggelaten, omdat geen Bird-beheerd template een headervariabele declareert, maar bij een template dat je werkruimte heeft gemaakt wordt het doorgestuurd, en zo krijgt een media-header utility- of marketingtemplate zijn afbeelding.
Bijvoorbeeld: een eenmalig-verificatiecodetemplate waarvan de body {{1}} is your verification code bevat en waarvan de knop de code kopieert, neemt de code als zowel een bodyparameter als een knopparameter, positioneel (geen name):
Codevoorbeeld
{
  "components": [
    { "type": "body", "parameters": [{ "type": "text", "text": "481920" }] },
    { "type": "button", "parameters": [{ "type": "text", "text": "481920" }] }
  ]
}

Categorie en afzender

De categorie van een template (authentication, utility of marketing) bepaalt hoe WhatsApp het bericht behandelt en, samen met het bestemmingsland, wat het kost.
Wie de afzender beheert bepaalt of je die opgeeft:
  • Een Bird-beheerd template (de slug begint met bird_) verstuurt vanaf het nummer dat Bird voor die categorie bewaart, dus laat from weg. Het instellen ervan geeft een 422 WhatsAppSenderNotAllowed terug.
  • Al het andere benoemt zijn eigen afzender in from: een servicebericht van elk type, en elk template dat je werkruimte heeft gemaakt. Het nummer moet er een zijn dat je werkruimte bezit. Het weglaten ervan geeft een 422 WhatsAppSenderRequired terug, en een nummer waarvandaan de werkruimte niet kan verzenden geeft een 422 WhatsAppSenderNotFound terug. Een zelfgemaakt template moet ook op hetzelfde WhatsApp Business Account staan als het nummer, anders geeft de verzending een 422 WhatsAppSenderWABAMismatch terug.
Telefoonnummer instellen behandelt beide typen nummers en hoe een eigen nummer wordt verbonden.

Serviceberichten

Gebruik in plaats van template precies één van text, image, video, audio, sticker, document, location, contact_cards of interactive. Alle negen zijn serviceberichten, dus ze vereisen een open klantenservicevenster. Elk ervan vereist ook from, een nummer dat je werkruimte bezit; de beheerde nummers van Bird kunnen het niet versturen.
  • text: { "body": "..." }, maximaal 4096 tekens. Voeg "preview_url": true toe om een linkpreview te renderen voor de eerste URL in body.
  • image, video, audio, sticker, document: elk neemt een publieke https URL die WhatsApp ophaalt bij verzending (url), dus een gesigneerde URL moet langer geldig zijn dan de verzending duurt. Een http URL wordt direct geweigerd. WhatsApp haalt het bestand zelf op, dus een URL die het niet kan bereiken, een die een niet-ondersteund type serveert, of een bestand boven de groottelimiet voor dat type wordt geaccepteerd en faalt daarna, met media_rejected op de last_error van het bericht en de eigen reden van WhatsApp in description. image, video en document nemen ook een optionele caption; document neemt ook een optionele filename; audio neemt een optionele voice-vlag voor een voicenoteweergave.
  • location: { "latitude": ..., "longitude": ... } (beide verplicht, decimale graden) plus optioneel name en address.
  • contact_cards: een array van maximaal vijf contacten die in één bericht worden gedeeld. De name van elke kaart heeft formatted_name nodig plus minstens één ander onderdeel (first_name, last_name, middle_name, prefix of suffix); phone_numbers, emails, urls en addresses nemen elk maximaal tien items, en org en birthday (als YYYY-MM-DD) zijn optioneel. Een phone_number in E.164 levert die kaart een knop op die een chat ermee opent.
  • interactive: bodytekst plus iets om op te tikken, in een van zes typen: antwoordknoppen, een lijstmenu, een linkknop, een mediacarrousel, of een enkele knop die de ontvanger om zijn locatie of telefoonnummer vraagt. Interactieve berichten behandelt de wire-structuur van elk type, de antwoorden die een tik oplevert, en de limieten.
Codevoorbeeld
{
  "to": "+16505551234",
  "from": "+13124495648",
  "text": { "body": "Your order shipped: https://example.com/track/A1B2C3", "preview_url": true }
}
Een request zonder content, of met meer dan één type, wordt geweigerd met een 422.

Een bericht citeren

Stel in_reply_to_message_id in op een WhatsApp bericht-ID om je bericht als antwoord erop te verzenden, op dezelfde manier als tikken op beantwoorden in de WhatsApp-app een bericht citeert. De ontvanger ziet je bericht met het geciteerde erboven, en het veld komt terug bij elke lezing van het bericht.
Het werkt ook andersom: een inkomend bericht dat WhatsApp als antwoord markeert bevat het ID van het geciteerde bericht in hetzelfde veld, en zo herken je op welk van je berichten een antwoord reageert. Een inkomend bericht dat WhatsApp niet als zodanig markeert bevat geen ID, en resolutie kan ook missen. Gebruik voor betrouwbare correlatie expliciete interactieve antwoord-ID's samen met de opgeslagen conversatie- of taakstatus van je applicatie. Uitgaande metadata blijft op het uitgaande record staan en wordt niet automatisch naar het antwoord gekopieerd.
Het citaat wordt opgelost voordat de verzending wordt geaccepteerd, dus een citaat dat niet kan worden gerenderd laat het request zelf falen en er wordt niets aangemaakt of in rekening gebracht. Een id dat geen bericht benoemt dat deze werkruimte bevat, of een bericht ouder dan de 15 dagen dat een bericht citeerbaar blijft, geeft een 404 E15071 WhatsAppReferencedMessageNotFound terug. Een id dat een bericht benoemt dat WhatsApp nooit heeft bereikt, of een bericht uit een andere conversatie dan de to en from van deze verzending, geeft een 422 E15072 WhatsAppMessageNotQuotable terug. Als Bird de store die de vraag beantwoordt niet kan bereiken, geeft de verzending een 503 E15073 WhatsAppMessageLookupUnavailable terug, wat het waard is om opnieuw te proberen. Citeren werkt bij zowel een templateverzending als een vrije-vormverzending.
Codevoorbeeld
{
  "to": "+16505551234",
  "from": "+13124495648",
  "in_reply_to_message_id": "wam_01kya19eknftrs2s6p82asmvnh",
  "text": { "body": "Yes, that slot is still free." }
}

Tags en metadata

Twee optionele velden koppelen je eigen context aan een bericht; beide komen terug bij API-lezingen en worden meegegeven op elk webhook-event voor het bericht:
  • tags: maximaal 20 gestructureerde { "name": ..., "value": ... }-labels voor dimensies met lage kardinaliteit waarop je filtert en rapporteert (een campagne, een experimentvariant). Namen en waarden accepteren ASCII-letters, cijfers, underscore en koppelteken; namen zijn beperkt tot 32 tekens en uniek binnen een verzending, waarden tot 64. Filter de berichtenlijst op tag (?tag=campaign of ?tag=campaign:launch-week), en de pagina Metrics splitst bezorging uit per tag.
  • metadata: één willekeurig JSON-object, maximaal 2 KB geserialiseerd, voor context per verzending die je niet als filterdimensie nodig hebt (een intern order-ID, een sessieverwijzing).
Codevoorbeeld
{
  "tags": [{ "name": "campaign", "value": "order-confirmations" }],
  "metadata": { "order_id": "ord_8271" }
}

Veldreferentie

VeldTypeVerplichtLimieten / opmerkingen
tostringjaEén ontvanger per bericht: een E.164-telefoonnummer, een business-scoped user ID, dat geen enkel eenmalig-verificatiecodetemplate accepteert, of een WhatsApp groeps-ID (wag_…), dat naar elke deelnemer van die groep stuurt
fromstring (E.164)nee**Laat weg bij een Bird-beheerd template, dat zijn eigen afzender kiest, en bij een groepsverzending, die het eigen nummer van de groep gebruikt; verplicht voor een servicebericht en voor een template dat je werkruimte heeft gemaakt, en moet een nummer zijn dat je werkruimte bezit
template.slugstringnee**Een templateslug die je werkruimte kan verzenden; Bird-beheerde slugs beginnen met bird_
template.languagestringnee*Taaltag van het template (en, pt-BR); laat weg om de standaardtaal van het template te verzenden
template.componentsarrayneeVult de variabelen van het template in; component type is body of button
template.components[].parameters[].namestringnee†De placeholder die deze waarde invult, zoals ref; verplicht en moet overeenkomen met de gedeclareerde namen van het template bij een template met benoemde parameters, weggelaten bij een positioneel template
interactiveobjectnee**Bodytekst plus één type tikbare content; een servicebericht, dus het heeft een open servicevenster nodig. Zie Interactieve berichten
in_reply_to_message_idstringneeEen WhatsApp bericht-ID dat deze werkruimte bevat, geciteerd in het bericht dat je verstuurt; wordt geëchood bij lezingen. Zie Een bericht citeren
tagsarrayneeMaximaal 20 {name, value}-labels; naam ≤ 32 tekens, waarde ≤ 64, namen uniek
metadataobjectneeWillekeurige JSON, maximaal 2 KB geserialiseerd
* language is optioneel; weglaten verstuurt de standaardtaal van het template. † name is verplicht op elke parameter bij een template met benoemde parameters. Laat het weg bij een positioneel template. Zie Componenten en parameters. ** Gebruik precies één van template of een content-veld voor serviceberichten (text, image, video, audio, sticker, document, location, interactive); zie Serviceberichten.

Het asynchrone model: wat 202 betekent

Een succesvolle verzending geeft 202 Accepted terug met een bericht-ID en status: accepted. De 202 wordt pas teruggegeven nadat de verzending duurzaam is geaccepteerd; het wordt nooit geaccepteerd en daarna stilzwijgend weggegooid. Harde fouten die je kunt oplossen falen direct met een 422: een ongeldige ontvanger, een onbekende templateslug of -taal, een parametermismatch, of een servicebericht verzonden in een gesloten klantenservicevenster (WhatsAppServiceWindowClosed). Een wallet zonder saldo is daar niet bij: de verzending wordt geaccepteerd, en het bericht eindigt rejected met insufficient_balance zodra Bird probeert het in rekening te brengen. Daadwerkelijke bezorging gebeurt asynchroon: het bericht gaat naar sent wanneer we het aan WhatsApp overdragen, en vervolgens naar een eindstatus (delivered of failed) wanneer de ontvangstbevestiging binnenkomt, gerapporteerd via events, webhooks en de read-endpoints. Een leesbevestiging wordt apart getoond als een read_at-tijdstempel en een whatsapp.read-event in plaats van als status.
Eén privacyopmerking: voor authentication-categorietemplates geeft de API nooit de ingevulde waarden terug. De 202-echo en elke latere lezing bevatten een lege components-array voor die berichten, zodat een verificatiecode nooit opnieuw verschijnt.

Veilig opnieuw proberen

Stuur de Idempotency-Key-header mee met een unieke waarde per logische verzending, en opnieuw proberen wordt veilig. Als je eerste request slaagde maar je het antwoord nooit zag (timeout, verbroken verbinding), geeft het opnieuw afspelen met dezelfde sleutel het oorspronkelijke resultaat terug in plaats van een dubbel bericht te verzenden en in rekening te brengen. Het opnieuw afgespeelde antwoord bevat een Idempotency-Replay-header. Zie idempotency voor sleutelformaat en bewaartermijn.

Het antwoord ontvangen

Inkomende berichten komen op dezelfde resource terecht als uitgaande, en elk ervan reset het servicevenster. WhatsApp-berichten ontvangen behandelt het lezen ervan via de API, het ophalen van de media die een contact heeft gestuurd, en de whatsapp.received-webhook.

Kosten en facturering

WhatsApp wordt per bericht geprijsd, op basis van de categorie van het template en het land van de ontvanger; zie WhatsApp-prijzen. Een bericht wordt in twee stappen in rekening gebracht, op twee verschillende momenten, en het cost-object op het bericht rapporteert beide:
VeldWat het isWanneer het binnenkomt
transaction_amountDe vergoeding van Bird voor het verwerken van de verzendingWanneer Bird de geaccepteerde verzending verwerkt, vóór dispatch
passthrough_amountMeta's aandeel in de berichtprijs, dat Bird doorberekentWanneer een toepasselijke delivered- of read-ontvangstbevestiging binnenkomt
amountDe som van de tot nu toe geprijsde componentenGroeit naarmate elk component binnenkomt
currency_codeDe valuta van de wallet van je organisatie, gedeeld door beide componentenBij het eerste component
Beide bedragen zijn decimale strings, exclusief belasting.
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.
De twee componenten worden op verschillende inputs geprijsd. De vergoeding van Bird gebruikt de categorie van het template dat je hebt verzonden en het land van de ontvanger, dat volgt uit de landcode van het telefoonnummer of, bij een verzending aan een business-scoped user ID, uit het tweeletterige prefix op dat ID. Meta's aandeel gebruikt de categorie die Meta zelf rapporteert op de toepasselijke ontvangstbevestiging, die kan afwijken van die van het template: Meta kan authentication-international rapporteren wanneer zijn bestemmings-, bedrijfslocatie- en geschiktheidsregels van toepassing zijn. Zie WhatsApp authentication-international-tarieven.
Wat cost weergeeft hangt af van hoe ver het bericht is gevorderd:
  • Bij de 202 is cost null. Er is nog niets geprijsd.
  • Na verwerking is transaction_amount ingesteld en is amount gelijk eraan. passthrough_amount blijft null.
  • Na een toepasselijke delivered- of read-ontvangstbevestiging vult een succesvol geregistreerde Meta-afschrijving passthrough_amount in, en amount weerspiegelt de geregistreerde componenten.
Een null-component betekent dat er geen bedrag is vastgelegd in die projectie; het is geen bewijs dat het bericht gratis was. Een component dat expliciet op nul is geprijsd leest "0.00000".
De twee afschrijvingen falen ook op verschillende manieren. De vergoeding van Bird faalt gesloten: wanneer die na de 202 niet kan worden verwerkt omdat de wallet de verzending niet kan dekken of de route geen geconfigureerde prijs heeft, eindigt het bericht rejected met foutcode insufficient_balance of price_not_found, en er wordt niets in rekening gebracht. Een rejected-bericht heeft WhatsApp nooit bereikt, en dat onderscheidt het van failed. Meta's aandeel faalt open: als de wallet ontoereikend is of het tarief ontbreekt wanneer de ontvangstbevestiging binnenkomt, wordt de afschrijving overgeslagen zonder de waargenomen berichtstatus terug te draaien. Je bezorging wordt nooit opgehouden door de tweede afschrijving.
Een bericht dat door Bird in rekening is gebracht behoudt die uitgaande afschrijving als de bezorging later mislukt. De Meta-vergoeding wordt verwerkt op basis van een toepasselijke delivered- of read-callback wanneer Meta reguliere prijzen rapporteert met een oplosbare categorie en bestemming. Beide callback-paden gebruiken dezelfde vergoedings-ID en vertrouwen op de deduplicatie van de factureringsservice. Reconcilieer herhaalde ontvangstbevestigingen tegen factureringsrecords in plaats van de berichtprojectie als een permanent afschrijvingsbewijs te behandelen. Service- of free-entry-prijzen kunnen het Meta-component op nul zetten; een onopgelost component is geen bewijs dat het bericht gratis was.
Gebruik het factureringsboek voor financiële reconciliatie. cost-velden van berichten zijn projecties van de afschrijvingen en kunnen achterlopen of onvolledig blijven. Zie WhatsApp-metrics voor het onderscheid tussen berichtwaarnemingen en factureringsrecords.
WhatsApp-events bevatten geen kosten. Om een van beide componenten te lezen, lees je het bericht terug met GET /v1/whatsapp/messages/{id}.

Volgende stappen