List WhatsApp messages
/v1/whatsapp/messagesfor await (const msg of bird.whatsapp.list({ status: ["delivered"] })) {
console.log(msg.id, msg.status);
}for msg in client.whatsapp.list(status=["delivered"]):
print(msg.id, msg.status)package main
import (
"context"
"fmt"
"log"
"os"
bird "github.com/messagebird/bird-sdk-go"
"github.com/messagebird/bird-sdk-go/option"
)
func main() {
client, err := bird.NewClient(option.WithAPIKey(os.Getenv("BIRD_API_KEY")))
if err != nil {
log.Fatal(err)
}
for msg, err := range client.Whatsapp.List(context.Background(), bird.WhatsappListParams{PhoneNumber: "+15551234567"}) {
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.Id)
}
}foreach ($bird->whatsapp->list(['status' => ['delivered']]) as $message) {
echo $message->getId(), ' ', $message->getStatus(), "\n";
}bird whatsapp listcurl -X GET "https://us1.platform.bird.com/v1/whatsapp/messages" \
-H "Authorization: Bearer $TOKEN" \
--url-query "limit=25"{
"data": [
{
"id": "wam_01kya1b3xdq7fe8m2v5t9rncgs",
"direction": "inbound",
"from": {
"phone_number": "+15550002222",
"bsuid": "US.AbC1",
"display_name": "Dana Reyes"
},
"to": {
"phone_number": "+15550001111",
"group_id": "wag_01krdgeqcxet5s7t44vh8rt9mg"
},
"text": {
"body": "Got it, thanks."
},
"status": "received",
"created_at": "2026-09-15T14:05:41Z"
},
{
"id": "wam_01kya19eknftrs2s6p82asmvnh",
"direction": "outbound",
"from": {
"phone_number": "+15550001111"
},
"to": {
"group_id": "wag_01krdgeqcxet5s7t44vh8rt9mg"
},
"text": {
"body": "The route sheet for Tuesday is up."
},
"status": "delivered",
"recipient_count": 2,
"delivered_count": 2,
"read_count": 1,
"created_at": "2026-09-15T14:03:10Z"
}
],
"next_cursor": null,
"prev_cursor": null,
"refresh_cursor": "eyJ2IjoxLCJzIjoiXCIyMDI2LTA1LTI1VDE0OjAzOjEwWlwiIiwiaSI6IjAxOTJmM2IxLTRjN2UtN2EyYi05ZDYxLThmM2E1YzJlN2I0MCJ9"
}
Returns the workspace's WhatsApp messages as a cursor-paginated list,
newest first, outbound and inbound alike. Each message carries the one
content object it was built from: template, or free-form text,
image, video, audio, sticker, document, location,
interactive or contact_cards. An inbound message carries
interactive_reply when the contact tapped a reply button or a list row,
on an interactive message or on a template's quick reply. An inbound
message whose content WhatsApp models and we do not carries unsupported
instead, naming the type rather than reading back empty.
Filter by direction, status, recipient (to), sender (from),
business-scoped user ID (bsuid), group (group_id), template category,
tag, or creation
time. to and from name the same ends of the message the response
does, and each accepts an E.164 phone number or a business-scoped user
ID. Pair either with direction to search a single side of the message.
Neither matches a group, so group_id is what narrows the list to one
group's messages.
Pass the response's next_cursor back as
starting_after to fetch the next page. To follow a single message's
delivery, use
Get a WhatsApp message
instead.
Messages are retained for 30 days. A created_after earlier than that
is accepted and raised to the retention bound rather than rejected, so a
wider window returns what is still retained instead of failing. There is no
way to read messages older than the window.
Query Parameters
limitintegerMaximum number of items to return per page.
starting_afterstringCursor from the next_cursor field of a previous list response. Returns items immediately after the cursor position in the current sort order.
ending_beforestringCursor from the prev_cursor or refresh_cursor field of a previous list response. Returns items immediately before the cursor position in the current sort order. prev_cursor returns the preceding page. refresh_cursor anchors at the first row of that response, which on a newest-first sort is how to fetch the items that have appeared since.
created_afterstringLimits the response to resources created at or after this timestamp. Combine it with created_before to select a time window. Use an RFC 3339 timestamp with a timezone offset.
created_beforestringLimits the response to resources created before this timestamp. Combine it with created_after to select a time window. Use an RFC 3339 timestamp with a timezone offset.
statusarrayFilter by status. Repeat the parameter to match any of several statuses.
directionstringFilter by whether the business sent the message (outbound) or received it from the contact (inbound).
Possible values: outbound, inbound
tostringFilter by recipient, exact match. The recipient is the contact on an outbound message and your business number on an inbound one, matching the to each message returns. Accepts an E.164 phone number, or a business-scoped user ID to name the contact. Only a contact is ever identified by a business-scoped user ID, so to=<business-scoped user ID> matches outbound messages only.
fromstringFilter by sender, exact match. The sender is your business number on an outbound message and the contact on an inbound one, matching the from each message returns. Accepts an E.164 phone number, or a business-scoped user ID to name the contact. Only a contact is ever identified by a business-scoped user ID, so from=<business-scoped user ID> matches inbound messages only.
phone_numberstringDeprecated: use to or from instead, which also match a business-scoped user ID. Filters by contact phone number (E.164 exact match), in either direction.
bsuidstringFilter by business-scoped user ID (Meta identifier), matching the contact in either direction. to and from also accept one, but each matches a single end of the message.
categorystringFilter by category.
Possible values (may grow over time): authentication, utility, marketing
group_idstringFilter by the WhatsApp group the message belongs to, in either direction: the group an outbound message was addressed to, or the group an inbound message arrived through. Matches the group_id on each message's to. It names one group, so there is no way to ask for the messages that belong to no group: omit it to list group and one-to-one messages together.
tagarrayFilter by tag. Accepts name to match any record carrying that tag name, or name:value to match a specific tag pair (for example category:welcome). Repeat the parameter to add more tags. A record must match every tag listed to be returned.
Response Payload
dataPage of WhatsApp messages, newest first.
Show child attributes
data.idID of the message, assigned when the send is accepted. Pass it as message_id to the get-message and list-events endpoints.
data.directionWhether the message was sent by the business (outbound) or received from the contact (inbound).
Possible values: outbound, inbound
data.fromSender of the message. On outbound messages, the business number it was sent from; on inbound, the WhatsApp contact.
Show child attributes
data.from.phone_numberPhone number in E.164 format, when known.
data.from.bsuidBusiness-scoped user ID, Meta's identifier for the WhatsApp user. Present only on the WhatsApp-user side of the message.
data.from.group_idThe group this address was addressed as, or reached through. It appears on a message's to and nowhere else: never on from, and never on an event's recipient. Outbound, it stands in for the recipient, because a group send names no single phone number. Inbound, it qualifies one: to carries the business phone_number that received the message and the group it arrived through, while from stays the participant who wrote it. Its presence on to is what tells a group message from a one-to-one one, in either direction.
data.from.usernamePresent only on a message received from a WhatsApp user, on from; never on an outbound send's to, where the profile is not known. Absent when the contact has not adopted one, and on a message received before this workspace started recording them. Same form as a number's own username (WhatsAppNumberProfile.username), without a leading @; a message cannot be addressed by it.
data.from.display_namePresent only on a message received from a WhatsApp user, on from; never on an outbound send's to, where the profile is not known. Absent when the message carries no profile, and on a message received before this workspace started recording them.
data.toRecipient of the message. On outbound messages, the WhatsApp contact; on inbound, the business number.
Show child attributes
data.to.phone_numberPhone number in E.164 format, when known.
data.to.bsuidBusiness-scoped user ID, Meta's identifier for the WhatsApp user. Present only on the WhatsApp-user side of the message.
data.to.group_idThe group this address was addressed as, or reached through. It appears on a message's to and nowhere else: never on from, and never on an event's recipient. Outbound, it stands in for the recipient, because a group send names no single phone number. Inbound, it qualifies one: to carries the business phone_number that received the message and the group it arrived through, while from stays the participant who wrote it. Its presence on to is what tells a group message from a one-to-one one, in either direction.
data.to.usernamePresent only on a message received from a WhatsApp user, on from; never on an outbound send's to, where the profile is not known. Absent when the contact has not adopted one, and on a message received before this workspace started recording them. Same form as a number's own username (WhatsAppNumberProfile.username), without a leading @; a message cannot be addressed by it.
data.to.display_namePresent only on a message received from a WhatsApp user, on from; never on an outbound send's to, where the profile is not known. Absent when the message carries no profile, and on a message received before this workspace started recording them.
data.templateThe template the message was sent from. For authentication templates the filled-in values are not returned.
Show child attributes
data.template.slugThe template's stable handle (for example bird_otp).
data.template.categoryThe category this message was priced at, recorded as it stood when the message was sent. For a template you authored this is the category Meta applies to the language the send resolved to, which can differ from the category declared on the template: Meta categorizes each language separately and may move one. A built-in bird_ template is priced at the single category the built-in declares, the same in every language.
data.template.languageThe canonical BCP-47 tag of the template variant that was sent.
data.template.componentsThe values that filled the template's placeholders. Empty for an authentication template, whose content is never returned.
Show child attributes
data.template.components.typeWhich part of the template this fills in.
body: the main text.button: a button's variable.header: the header's text, media or location.carousel: the cards.
Possible values (may grow over time): header, body, button, carousel
data.template.components.parametersThe values that fill this part's placeholders. A positional template takes them in {{n}} placeholder order; a template with named parameters requires each parameter's name to match one the template declares, and order then carries no meaning. Send it on every part except carousel, which carries its values on cards. Send no button part at all for a button that takes no value, such as a quick_reply or request_contact_info button, or a url button whose address has no placeholder: a part the template has no slot for is refused here, before the message is sent and charged.
Show child attributes
data.template.components.parameters.typeThe kind of value this parameter carries, which decides which of the fields below to send.
data.template.components.parameters.textThe value substituted into the placeholder, as a plain string. Send it on a text parameter.
data.template.components.parameters.urlPublic https URL of the file a media header shows. Send it on an image, video, gif or document parameter. WhatsApp fetches it at send time, so it must still be reachable then, the same way a free-form media message's url must.
data.template.components.parameters.locationThe point on the map a location header opens. Send it on a location parameter.
Show child attributes
data.template.components.parameters.location.latitudeLatitude in decimal degrees.
data.template.components.parameters.location.longitudeLongitude in decimal degrees.
data.template.components.parameters.location.nameName of the place, shown above the address.
data.template.components.parameters.location.addressStreet address of the place. Shown only when name is also set.
data.template.components.parameters.nameRequired when the template declares named parameters: the placeholder this value fills (for example first_name), matching exactly one of the names the template declares. Name every parameter in that case; order does not matter once names are supplied. Omit this field for a positional template, which takes its values in {{n}} order instead. Sending the wrong set of names, or leaving one out that the template requires, returns a 422 WhatsAppTemplateParameterMismatch.
data.template.components.cardsThe values that fill each card of a carousel. Send it only on a carousel part. A carousel sends exactly the number of cards its template was approved with, so every card needs an entry.
Show child attributes
data.template.components.cards.componentsThe values that fill this card's blocks.
Show child attributes
data.template.components.cards.components.typeWhich part of the card this fills in.
header: the card's image or video.body: its text.button: a button's variable.
Possible values (may grow over time): header, body, button
data.template.components.cards.components.parametersThe values that fill this part's placeholders, in placeholder order.
Show child attributes
data.template.components.cards.components.parameters.typeThe kind of value this parameter carries, which decides which of the fields below to send.
data.template.components.cards.components.parameters.textThe value substituted into the placeholder, as a plain string. Send it on a text parameter.
data.template.components.cards.components.parameters.urlPublic https URL of the file a media header shows. Send it on an image, video, gif or document parameter. WhatsApp fetches it at send time, so it must still be reachable then, the same way a free-form media message's url must.
data.template.components.cards.components.parameters.locationThe point on the map a location header opens. Send it on a location parameter.
Show child attributes
data.template.components.cards.components.parameters.location.latitudeLatitude in decimal degrees.
data.template.components.cards.components.parameters.location.longitudeLongitude in decimal degrees.
data.template.components.cards.components.parameters.location.nameName of the place, shown above the address.
data.template.components.cards.components.parameters.location.addressStreet address of the place. Shown only when name is also set.
data.template.components.cards.components.parameters.nameRequired when the template declares named parameters: the placeholder this value fills (for example first_name), matching exactly one of the names the template declares. Name every parameter in that case; order does not matter once names are supplied. Omit this field for a positional template, which takes its values in {{n}} order instead. Sending the wrong set of names, or leaving one out that the template requires, returns a 422 WhatsAppTemplateParameterMismatch.
data.textText the message carried.
Show child attributes
data.text.bodyThe message text.
data.imageImage the message carried.
Show child attributes
data.image.idID of the stored file, to pass as media_id when fetching it. Absent on an outbound message, whose file we never stored.
data.image.urlWhere to fetch the media. On an inbound message this is a Bird URL you fetch with your API key; it is absent when the file could not be retrieved from WhatsApp. It stays populated after the stored bytes expire, and the link returns 410 from then on. On an outbound message it is the URL the sender supplied, whose availability is the sender's to guarantee.
data.image.mime_typeMedia type WhatsApp reported for the file, for example image/jpeg. Absent on outbound messages.
data.image.captionText shown beneath the image. Absent when the sender wrote none.
data.videoVideo the message carried.
Show child attributes
data.video.idID of the stored file, to pass as media_id when fetching it. Absent on an outbound message, whose file we never stored.
data.video.urlWhere to fetch the media. On an inbound message this is a Bird URL you fetch with your API key; it is absent when the file could not be retrieved from WhatsApp. It stays populated after the stored bytes expire, and the link returns 410 from then on. On an outbound message it is the URL the sender supplied, whose availability is the sender's to guarantee.
data.video.mime_typeMedia type WhatsApp reported for the file, for example image/jpeg. Absent on outbound messages.
data.video.captionText shown beneath the video. Absent when the sender wrote none.
data.audioAudio the message carried.
Show child attributes
data.audio.idID of the stored file, to pass as media_id when fetching it. Absent on an outbound message, whose file we never stored.
data.audio.urlWhere to fetch the media. On an inbound message this is a Bird URL you fetch with your API key; it is absent when the file could not be retrieved from WhatsApp. It stays populated after the stored bytes expire, and the link returns 410 from then on. On an outbound message it is the URL the sender supplied, whose availability is the sender's to guarantee.
data.audio.mime_typeMedia type WhatsApp reported for the file, for example image/jpeg. Absent on outbound messages.
data.audio.voiceWhether this is a voice note rather than an attached audio file. A voice note auto-downloads in the WhatsApp client and can be transcribed for the recipient.
data.stickerSticker the message carried.
Show child attributes
data.sticker.idID of the stored file, to pass as media_id when fetching it. Absent on an outbound message, whose file we never stored.
data.sticker.urlWhere to fetch the media. On an inbound message this is a Bird URL you fetch with your API key; it is absent when the file could not be retrieved from WhatsApp. It stays populated after the stored bytes expire, and the link returns 410 from then on. On an outbound message it is the URL the sender supplied, whose availability is the sender's to guarantee.
data.sticker.mime_typeMedia type WhatsApp reported for the file, for example image/jpeg. Absent on outbound messages.
data.sticker.animatedWhether the sticker is animated. Absent on an outbound message.
data.documentDocument the message carried.
Show child attributes
data.document.idID of the stored file, to pass as media_id when fetching it. Absent on an outbound message, whose file we never stored.
data.document.urlWhere to fetch the media. On an inbound message this is a Bird URL you fetch with your API key; it is absent when the file could not be retrieved from WhatsApp. It stays populated after the stored bytes expire, and the link returns 410 from then on. On an outbound message it is the URL the sender supplied, whose availability is the sender's to guarantee.
data.document.mime_typeMedia type WhatsApp reported for the file, for example image/jpeg. Absent on outbound messages.
data.document.captionText shown beneath the document. Absent when the sender wrote none.
data.document.filenameThe sender's own name for the file.
data.locationLocation the message carried.
Show child attributes
data.location.latitudeLatitude in decimal degrees.
data.location.longitudeLongitude in decimal degrees.
data.location.nameName of the place. Absent when the sender shared a plain pin.
data.location.addressStreet address of the place. Shown only when name is also set.
data.location.urlLink to the place, which WhatsApp includes mainly for business locations. Present on an inbound message when the sender's client supplied one, and absent on a message you sent, since sending a location does not support this field.
data.contact_cardsContact cards on this message: cards the contact shared, either by tapping a button that asked for their number or by sending one from their address book, or the cards this workspace sent.
Show child attributes
data.contact_cards.originWhy the card arrived. contact_request means the contact tapped a button this workspace sent asking for their number, which is the only signal that the message answers that ask; other means they shared a card in the chat. Open enum: treat an unrecognized value as a way of sharing added since. Set on a card the contact shared; absent on one this workspace sent.
Possible values (may grow over time): contact_request, other
data.contact_cards.vcardThe contact's card in vCard format. WhatsApp sends it on a card shared in the chat and omits it on a button tap, which carries the number alone. Set on a card the contact shared; absent on one this workspace sent.
data.contact_cards.nameThe contact's name, when the card carries one.
Show child attributes
data.contact_cards.name.formatted_nameThe whole name as the contact's device renders it.
data.contact_cards.name.first_namedata.contact_cards.name.middle_namedata.contact_cards.name.last_namedata.contact_cards.name.prefixdata.contact_cards.name.suffixdata.contact_cards.orgWhere the contact works, when the card carries it.
Show child attributes
data.contact_cards.org.companydata.contact_cards.org.departmentdata.contact_cards.org.titledata.contact_cards.birthdayThe contact's birthday, which WhatsApp sends as YYYY-MM-DD. Passed through as text rather than typed as a date: the value comes off the contact's own device unvalidated, and a card we could not parse would otherwise have to lose the field or fail the whole read.
data.contact_cards.phone_numbersThe numbers on the card. A button tap carries the contact's own number here, which is the point of asking.
Show child attributes
data.contact_cards.phone_numbers.phone_numberThe number as the card holds it, normalized to E.164 where we can parse it. A card is whatever the contact's device stored, so a number that no country's numbering plan accepts, an extension among them, is passed through exactly as it arrived rather than dropped. Parse defensively: most values are E.164 and none is guaranteed to be.
data.contact_cards.phone_numbers.typeThe label attached to this value, for example CELL, Home or iPhone. Free text: WhatsApp defines no vocabulary. A label on a received card is lowercased; one this workspace sent reads back exactly as sent.
data.contact_cards.emailsShow child attributes
data.contact_cards.emails.emaildata.contact_cards.emails.typeThe label attached to this value, for example CELL, Home or iPhone. Free text: WhatsApp defines no vocabulary. A label on a received card is lowercased; one this workspace sent reads back exactly as sent.
data.contact_cards.urlsShow child attributes
data.contact_cards.urls.urlThe address as the card holds it, which is often bare rather than a full URL, so it is passed through as text rather than validated.
data.contact_cards.urls.typeThe label attached to this value, for example CELL, Home or iPhone. Free text: WhatsApp defines no vocabulary. A label on a received card is lowercased; one this workspace sent reads back exactly as sent.
data.contact_cards.addressesShow child attributes
data.contact_cards.addresses.streetdata.contact_cards.addresses.citydata.contact_cards.addresses.statedata.contact_cards.addresses.zipdata.contact_cards.addresses.countrydata.contact_cards.addresses.country_codeThe country as the card holds it, left exactly as WhatsApp sent it: it describes a postal address rather than a routing destination.
data.contact_cards.addresses.typeThe label attached to this value, for example CELL, Home or iPhone. Free text: WhatsApp defines no vocabulary. A label on a received card is lowercased; one this workspace sent reads back exactly as sent.
data.interactiveInteractive content the message carried. Outbound only: a contact cannot send one. A tap on a reply button or a list row reads back as interactive_reply on the contact's inbound message; a cta_url link sends nothing back, and the two request kinds are answered by an inbound location or contact_cards message.
Show child attributes
data.interactive.typeWhich kind of interactive message this is, and which field carries it.
data.interactive.headerWhat was shown above the body. Absent when the message carried no header.
Show child attributes
data.interactive.header.typeWhich kind of header this is, and which field carries it.
data.interactive.header.textThe line of text shown above the body.
data.interactive.header.urlThe URL of the file shown above the body, as the send supplied it. Interactive content is outbound only, so Bird neither stores nor proxies the file.
data.interactive.body_textThe message's main text.
data.interactive.footer_textThe small print below the body. Absent when the message carried none.
data.interactive.buttonsThe buttons the message offered, in the order shown.
Show child attributes
data.interactive.buttons.typeWhich kind of button this is, and which field carries it.
data.interactive.buttons.quick_replyThe button's label and the handle it sends back.
Show child attributes
data.interactive.buttons.quick_reply.slugThe handle the button carries back, never shown to the recipient. On a tap on a template's quick-reply button, it is the payload that template declared.
data.interactive.buttons.quick_reply.textThe label the recipient saw.
data.interactive.buttons.cta_urlThe button's label and the address it opens.
Show child attributes
data.interactive.buttons.cta_url.textThe button's label.
data.interactive.buttons.cta_url.urlThe address the button opens, as the send supplied it.
data.interactive.listThe menu the message offered.
Show child attributes
data.interactive.list.button_textThe label of the button that opens the menu.
data.interactive.list.sectionsThe groups of options in the menu, in the order shown.
Show child attributes
data.interactive.list.sections.titleThe group's heading, shown above its rows.
data.interactive.list.sections.rowsThe options in this group, in the order shown.
Show child attributes
data.interactive.list.sections.rows.slugThe handle the row carries back, never shown to the recipient.
data.interactive.list.sections.rows.textThe row's label, shown as its title in the menu.
data.interactive.list.sections.rows.descriptionThe second line under the label. Absent when the row carried none.
data.interactive.cta_urlThe link button the message offered.
Show child attributes
data.interactive.cta_url.textThe button's label.
data.interactive.cta_url.urlThe address the button opens, as the send supplied it.
data.interactive.cardsThe cards the message offered, in the order they appeared, left to right.
Show child attributes
data.interactive.cards.headerThe image or video shown at the top of the card.
Show child attributes
data.interactive.cards.header.typeWhich kind of header this is, and which field carries it.
data.interactive.cards.header.textThe line of text shown above the body.
data.interactive.cards.header.urlThe URL of the file shown above the body, as the send supplied it. Interactive content is outbound only, so Bird neither stores nor proxies the file.
data.interactive.cards.body_textThe card's own text. Absent when the card carried none.
data.interactive.cards.buttonsThe buttons the card offered, in the order shown.
Show child attributes
data.interactive.cards.buttons.typeWhich kind of button this is, and which field carries it.
data.interactive.cards.buttons.quick_replyThe button's label and the handle it sends back.
Show child attributes
data.interactive.cards.buttons.quick_reply.slugThe handle the button carries back, never shown to the recipient. On a tap on a template's quick-reply button, it is the payload that template declared.
data.interactive.cards.buttons.quick_reply.textThe label the recipient saw.
data.interactive.cards.buttons.cta_urlThe button's label and the address it opens.
Show child attributes
data.interactive.cards.buttons.cta_url.textThe button's label.
data.interactive.cards.buttons.cta_url.urlThe address the button opens, as the send supplied it.
data.in_reply_to_message_idThe message this one answers. On an inbound message it is what WhatsApp reports as the reply's target: a tap on a button or a list row, and equally a text or media message the contact sent as a quoted reply. An outbound message echoes the in_reply_to_message_id it was sent with. Absent when the message answers nothing, and absent on an inbound message whose target we cannot match to a message we hold, which is the case for one sent before this workspace started recording them or one already past the 15-day window we keep provider ids for.
data.interactive_replyWhat the contact tapped, on a message answering an interactive message or a template's quick-reply button. Inbound only.
Show child attributes
data.interactive_reply.typeWhich kind of tap this reply came from, and which field carries it.
data.interactive_reply.buttonThe button the contact tapped, as you declared it. On a reply to a template's quick-reply button, slug is the button's payload, which WhatsApp sets to the button's own label.
Show child attributes
data.interactive_reply.button.slugThe handle the button carries back, never shown to the recipient. On a tap on a template's quick-reply button, it is the payload that template declared.
data.interactive_reply.button.textThe label the recipient saw.
data.interactive_reply.listThe row the contact chose, as you declared it. description is present only when the row carried one.
Show child attributes
data.interactive_reply.list.slugThe handle the row carries back, never shown to the recipient.
data.interactive_reply.list.textThe row's label, shown as its title in the menu.
data.interactive_reply.list.descriptionThe second line under the label. Absent when the row carried none.
data.unsupportedSet when the contact sent content we do not model, naming the WhatsApp content type so the message is not silently empty. Inbound only.
Show child attributes
data.unsupported.typeThe WhatsApp content type we did not model. unsupported is not a placeholder here: WhatsApp reports its own unsupported type for a message its own clients cannot render, and that arrives as this value. Open enum: WhatsApp adds content types over time, so treat an unrecognized value as a future type rather than an error.
Possible values (may grow over time): reaction, interactive, button, order, system, unsupported
data.reactionsEmoji reactions standing on this message right now, one per sender. Absent when the message has none. A reaction that was replaced by a different emoji, or taken back, is not listed; the message's reaction log keeps that history. WhatsApp accepts a reaction on a message up to 30 days old, and we keep provider ids for 15, so a reaction placed on a message older than that cannot be matched to it and does not appear here.
Show child attributes
data.reactions.emojiThe emoji, as WhatsApp sent it. It is not normalized, so two emoji that render identically can differ byte for byte and compare unequal.
data.reactions.fromWho reacted. On a group message this is what tells one participant's reaction from another's. On a one-to-one message it is your business number on a reaction you placed and the contact on one they placed, which is why it is here rather than inferred from the message's direction.
Show child attributes
data.reactions.from.phone_numberPhone number in E.164 format, when known.
data.reactions.from.bsuidBusiness-scoped user ID, Meta's identifier for the WhatsApp user. Present only on the WhatsApp-user side of the message.
data.reactions.from.group_idThe group this address was addressed as, or reached through. It appears on a message's to and nowhere else: never on from, and never on an event's recipient. Outbound, it stands in for the recipient, because a group send names no single phone number. Inbound, it qualifies one: to carries the business phone_number that received the message and the group it arrived through, while from stays the participant who wrote it. Its presence on to is what tells a group message from a one-to-one one, in either direction.
data.reactions.from.usernamePresent only on a message received from a WhatsApp user, on from; never on an outbound send's to, where the profile is not known. Absent when the contact has not adopted one, and on a message received before this workspace started recording them. Same form as a number's own username (WhatsAppNumberProfile.username), without a leading @; a message cannot be addressed by it.
data.reactions.from.display_namePresent only on a message received from a WhatsApp user, on from; never on an outbound send's to, where the profile is not known. Absent when the message carries no profile, and on a message received before this workspace started recording them.
data.statusdata.recipient_countHow many recipients a group send was addressed to, taken when the send was accepted. It is the group's membership at that moment, not its membership now: someone joining through the invite link while the message is in flight does not receive it and does not change this count.
Absent on a one-to-one message, along with delivered_count and
read_count. A message with one recipient has no fan-out to report, and
its delivery is what status, delivered_at and read_at already say.
Absent for the same reason on a group message sent before Bird recorded
the count, and on a send to a group nobody had joined yet: there is no
denominator to report, and none can be recovered after the fact, since
membership has moved on. to.group_id is what tells a group message from
a one-to-one one in every case, including those two. With no denominator
to resolve against, status is read as stored, the way a one-to-one
message's is: it reaches sent when the message is handed to WhatsApp and
stops there, because delivery is confirmed per participant and a send with
no participants collects no confirmations.
It is also the denominator status is resolved against: on a group
message status reports the furthest point every recipient has
reached, so it turns delivered only once delivered_count equals this
number, and stays sent while some have confirmed and others have not.
failed and rejected are never per recipient: there is one hand-off to
the WhatsApp network and one way for that to be refused. delivered_at
and read_at are the first recipient's, not the last.
data.delivered_countHow many of the recipient_count recipients WhatsApp has confirmed the
message reached. A recipient who reported only a read counts here too:
WhatsApp skips the delivery receipt when someone is already looking at
the chat, so waiting for one would leave that person uncounted for ever.
Absent on a one-to-one message, which has no fan-out to count, and on a
group message with no recipient_count to count against.
data.read_countHow many of the recipient_count recipients have opened the message.
Read receipts do not move status, which has no read value; they
surface here and in read_at.
Absent on a one-to-one message, which has no fan-out to count, and on a
group message with no recipient_count to count against.
data.last_errorFailure detail for a message that did not reach the recipient. Present only when the message failed or was rejected.
Show child attributes
data.last_error.codeStandardized failure reason:
insufficient_balance: The workspace wallet could not fund the send.price_not_found: No price was configured for the destination and template.internal_error: An unexpected service failure occurred.undeliverable: The recipient could not be reached.service_window_expired: The 24-hour service window closed; send a template.rate_limited: The send was throttled.recipient_suppressed: The recipient is on the workspace suppression list.media_rejected: WhatsApp could not fetch the media URL, or refused the file it found there;descriptioncarries its reason.
This is an open enum. Accept unrecognized values.
Possible values (may grow over time): insufficient_balance, price_not_found, internal_error, undeliverable, service_window_expired, rate_limited, recipient_suppressed, media_rejected
data.last_error.descriptionHuman-readable explanation of the failure.
data.last_error.meta_error_codeRaw error code from the WhatsApp Cloud API, when available, for low-level debugging.
data.last_error.occurred_atWhen the failure occurred.
data.created_atWhen the message was accepted for delivery.
data.sent_atWhen the message was handed to the WhatsApp network. Null until then.
data.delivered_atWhen delivery was confirmed. Null until then.
data.read_atWhen the message was read. On an outbound message this is the recipient opening it. On an inbound one it is when Bird acknowledged the message to WhatsApp for the business, which a read receipt sets. Null until then.
data.costWhat the message cost, split into Bird's charge and any third-party fees passed through. Null on an inbound message, which is never priced, on an outbound message that has not been priced yet, and on one rejected before pricing. The rate depends on the message category and the recipient's country.
Show child attributes
data.cost.amountTotal charged, as a decimal string: the sum of the components below. Net of tax, which applies to your wallet balance rather than to an individual charge.
data.cost.currency_codeISO 4217 currency code. Every component is denominated in this currency.
data.cost.transaction_amountWhat we charged to carry the message, as a decimal string. null when this component was not priced; "0.00000" when it priced at zero.
data.cost.passthrough_amountThird-party fees we pass on, as a decimal string, such as US 10DLC carrier surcharges. null when this component was not priced; "0.00000" when it priced at zero.
data.tagsStructured {name, value} filter labels applied to this message.
Show child attributes
data.tags.nameTag name. ASCII letters, digits, underscore, and hyphen only. Case-sensitive. Maximum 32 characters.
data.tags.valueTag value. ASCII letters, digits, underscore, and hyphen only. Case-sensitive. Maximum 64 characters.
data.metadataArbitrary JSON metadata stored on the message.
next_cursorCursor for the next page. Pass back as starting_after to advance forward. null when no next page exists.
prev_cursorCursor for the previous page. Pass back as ending_before to step backward. null when no previous page exists.
refresh_cursorRefresh anchor, the first row of this response. Pass back as ending_before to fetch what precedes it in the current sort order. On a newest-first sort those are the items that have appeared since; on any other sort they are the items that sort earlier, so refreshing such a list means re-fetching it instead. Non-null whenever data is non-empty; null only on an empty page. Distinct from prev_cursor.
Related resources
Continue with the documentation, guides and examples for this topic.