---
title: "Get a message in a WhatsApp conversation"
canonical: "https://bird.com/docs/api/reference/get-whatsapp-conversation-message"
---

# Get a message in a WhatsApp conversation

`GET /v1/whatsapp/conversations/{conversation_id}/messages/{message_id}`

Returns one message in a conversation. It is the same object [get a WhatsApp message](/docs/api/reference/get-whatsapp-message) returns, but read from the conversation's own store, so a message is readable here as soon as the conversation records it.

Returns `404` when the message does not belong to this conversation, or when it is past the 30-day message retention.

## Servers

- `https://{region}.platform.bird.com`: Regional API endpoint. Use the host for the region your organization is hosted in. Official Bird SDKs and the CLI select it automatically from your API key, so you rarely need to set it by hand.
  - `region`: `us1`, `eu1`. Default: `us1`. The region your organization's data is hosted in.
- `https://platform.bird.com`: Region-independent endpoint for authentication and account administration.
- `http://localhost:8080`: Local development.

## Authentication

Use one authentication option:

- `BearerAuth`: HTTP bearer authentication. Pass the API key as a bearer token in the `Authorization` header. Workspace
keys use `bk_{region}_*`; organization master keys use `bm_{region}_*`.
The prefix identifies the region and selects the
API endpoint. Official Bird SDKs and the CLI derive the region from the key.

- `CookieAuth`: Send `bird_session` in cookie. Session cookie set after signing in to the Bird dashboard. The cookie
value is an opaque session token; no session data is stored in the cookie
itself.

## Code samples

**TypeScript**

```ts
const message = await bird.whatsapp.conversations.getMessage(
  "wacv_01krdgeqcxet5s7t44vh8rt9mg",
  "wam_01krdgeqcxet5s7t44vh8rt9mg",
);
console.log(message.direction, message.status);
```

Examples: [TypeScript](/docs/api/reference/get-whatsapp-conversation-message.ts.md) · [Python](/docs/api/reference/get-whatsapp-conversation-message.py.md) · [Go](/docs/api/reference/get-whatsapp-conversation-message.go.md) · [PHP](/docs/api/reference/get-whatsapp-conversation-message.php.md) · [CLI](/docs/api/reference/get-whatsapp-conversation-message.cli.md) · [MCP](/docs/api/reference/get-whatsapp-conversation-message.mcp.md) · [cURL](/docs/api/reference/get-whatsapp-conversation-message.curl.md)

## Example response `200`

```json
{
  "id": "wam_01krdgeqcxet5s7t44vh8rt9mg",
  "direction": "inbound",
  "from": {
    "phone_number": "+15550001111",
    "bsuid": "US.13491208655302741918",
    "display_name": "Jamie Rivera"
  },
  "to": {
    "phone_number": "+15550001234"
  },
  "text": {
    "body": "Can I change the delivery address on my order?"
  },
  "status": "received",
  "created_at": "2026-09-23T10:35:00Z"
}
```

## Path parameters

- `conversation_id` (string, required): Conversation identifier. Starts with `wacv_`.
- `message_id` (string, required): Message identifier. Starts with `wam_`.

## Header parameters

- `X-Workspace-Id` (string, optional): Workspace context for the request. Required for dashboard authentication and master keys. Workspace keys and access tokens already identify their workspace; send that workspace or omit the header. A different one is rejected.
- `X-Organization-Id` (string, optional): Organization context for the request. Required for dashboard authentication. An API key or access token carries its own organization, so send either that organization or no header at all; a different one is rejected.

## Response body

- `id` (string, required): ID of the message, assigned when the send is accepted. Pass it as `message_id` to the get-message and list-events endpoints.
- `direction` (string, required)

  Whether the message was sent by the business (`outbound`) or received from the contact (`inbound`).

  Possible values: `outbound`, `inbound`
- `from` (object, required): Sender of the message. On outbound messages, the business number it was sent from; on inbound, the WhatsApp contact.
- `from.phone_number` (string): Phone number in E.164 format, when known.
- `from.bsuid` (string): Business-scoped user ID, Meta's identifier for the WhatsApp user. Present only on the WhatsApp-user side of the message.
- `from.group_id` (string): The 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.
- `from.username` (string): Present 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.
- `from.display_name` (string): Present 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.
- `to` (object, required): Recipient of the message. On outbound messages, the WhatsApp contact; on inbound, the business number.
- `to.phone_number` (string): Phone number in E.164 format, when known.
- `to.bsuid` (string): Business-scoped user ID, Meta's identifier for the WhatsApp user. Present only on the WhatsApp-user side of the message.
- `to.group_id` (string): The 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.
- `to.username` (string): Present 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.
- `to.display_name` (string): Present 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.
- `template` (object): The template the message was sent from. For authentication templates the filled-in values are not returned.
- `template.slug` (string, required): The template's stable handle (for example `bird_otp`).
- `template.category` (string, required)

  The 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.

  Possible values (may grow over time): `authentication`, `utility`, `marketing`
- `template.language` (string, required): The canonical BCP-47 tag of the template variant that was sent.
- `template.components` (array of object, required): The values that filled the template's placeholders. Empty for an authentication template, whose content is never returned.
- `template.components.type` (string, required)

  Which part of the template this fills in.

  - `body`: the main text.
  - `button`: one button's variable, a part per button; there is no `buttons` part.
  - `header`: the header's text, media or location.
  - `carousel`: the cards.

  Possible values (may grow over time): `header`, `body`, `button`, `carousel`
- `template.components.parameters` (array of object): The values for this part's placeholders, in `{{n}}` order unless the template names them. Send it on every part except `carousel`, which uses `cards`. Send no `button` part for a button that takes no value.
- `template.components.parameters.type` (string, required)

  The kind of value this parameter carries, which decides which of the fields below to send.

  Possible values (may grow over time): `text`, `image`, `video`, `gif`, `document`, `location`
- `template.components.parameters.text` (string): The value substituted into the placeholder, as a plain string. Send it on a `text` parameter.
- `template.components.parameters.url` (string): Public `https` URL of the file a media header shows, fetched by WhatsApp at send time. Send it on an `image`, `video`, `gif` or `document` parameter.
- `template.components.parameters.filename` (string): Name the recipient sees on the document, including the extension. Without it, the document arrives untitled. Send it only on a `document` parameter; Bird rejects it on any other kind.
- `template.components.parameters.location` (object): The point on the map a location header opens. Send it on a `location` parameter.
- `template.components.parameters.location.latitude` (number, required): Latitude in decimal degrees.
- `template.components.parameters.location.longitude` (number, required): Longitude in decimal degrees.
- `template.components.parameters.location.name` (string): Name of the place, shown above the address.
- `template.components.parameters.location.address` (string): Street address of the place. Shown only when `name` is also set.
- `template.components.parameters.name` (string): The placeholder this value fills, for a template with named parameters (for example `first_name`); name every parameter then. Omit it for a positional template, which takes values in `{{n}}` order.
- `template.components.cards` (array of object): Values for each carousel card, one entry per card the template was approved with. Send it only on a `carousel` part.
- `template.components.cards.components` (array of object, required): The values that fill this card's blocks.
- `template.components.cards.components.type` (string, required)

  Which 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`
- `template.components.cards.components.parameters` (array of object): The values that fill this part's placeholders, in placeholder order.
- `template.components.cards.components.parameters.type` (string, required)

  The kind of value this parameter carries, which decides which of the fields below to send.

  Possible values (may grow over time): `text`, `image`, `video`, `gif`, `document`, `location`
- `template.components.cards.components.parameters.text` (string): The value substituted into the placeholder, as a plain string. Send it on a `text` parameter.
- `template.components.cards.components.parameters.url` (string): Public `https` URL of the file a media header shows, fetched by WhatsApp at send time. Send it on an `image`, `video`, `gif` or `document` parameter.
- `template.components.cards.components.parameters.filename` (string): Name the recipient sees on the document, including the extension. Without it, the document arrives untitled. Send it only on a `document` parameter; Bird rejects it on any other kind.
- `template.components.cards.components.parameters.location` (object): The point on the map a location header opens. Send it on a `location` parameter.
- `template.components.cards.components.parameters.location.latitude` (number, required): Latitude in decimal degrees.
- `template.components.cards.components.parameters.location.longitude` (number, required): Longitude in decimal degrees.
- `template.components.cards.components.parameters.location.name` (string): Name of the place, shown above the address.
- `template.components.cards.components.parameters.location.address` (string): Street address of the place. Shown only when `name` is also set.
- `template.components.cards.components.parameters.name` (string): The placeholder this value fills, for a template with named parameters (for example `first_name`); name every parameter then. Omit it for a positional template, which takes values in `{{n}}` order.
- `text` (object): Text the message carried.
- `text.body` (string, required): The message text.
- `image` (object): Image the message carried.
- `image.id` (string): ID of the stored file, to pass as `media_id` when fetching it. Absent on a message sent through Bird, whose file we never stored.
- `image.url` (string): Where to fetch the media. On an inbound message, or one from the WhatsApp Business app, this is a Bird URL you fetch with your API key. The file expires with the message, 30 days after it arrives, and the link returns `404` from then on. If the file is gone while the message is still readable, the link returns `410`. On a message sent through Bird it is the URL the sender supplied, whose availability is the sender's to guarantee.
- `image.mime_type` (string): Media type WhatsApp reported for the file, for example `image/jpeg`. Absent on messages sent through Bird.
- `image.caption` (string): Text shown beneath the image. Absent when the sender wrote none.
- `video` (object): Video the message carried.
- `video.id` (string): ID of the stored file, to pass as `media_id` when fetching it. Absent on a message sent through Bird, whose file we never stored.
- `video.url` (string): Where to fetch the media. On an inbound message, or one from the WhatsApp Business app, this is a Bird URL you fetch with your API key. The file expires with the message, 30 days after it arrives, and the link returns `404` from then on. If the file is gone while the message is still readable, the link returns `410`. On a message sent through Bird it is the URL the sender supplied, whose availability is the sender's to guarantee.
- `video.mime_type` (string): Media type WhatsApp reported for the file, for example `image/jpeg`. Absent on messages sent through Bird.
- `video.caption` (string): Text shown beneath the video. Absent when the sender wrote none.
- `audio` (object): Audio the message carried.
- `audio.id` (string): ID of the stored file, to pass as `media_id` when fetching it. Absent on a message sent through Bird, whose file we never stored.
- `audio.url` (string): Where to fetch the media. On an inbound message, or one from the WhatsApp Business app, this is a Bird URL you fetch with your API key. The file expires with the message, 30 days after it arrives, and the link returns `404` from then on. If the file is gone while the message is still readable, the link returns `410`. On a message sent through Bird it is the URL the sender supplied, whose availability is the sender's to guarantee.
- `audio.mime_type` (string): Media type WhatsApp reported for the file, for example `image/jpeg`. Absent on messages sent through Bird.
- `audio.voice` (boolean): Whether 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.
- `sticker` (object): Sticker the message carried.
- `sticker.id` (string): ID of the stored file, to pass as `media_id` when fetching it. Absent on a message sent through Bird, whose file we never stored.
- `sticker.url` (string): Where to fetch the media. On an inbound message, or one from the WhatsApp Business app, this is a Bird URL you fetch with your API key. The file expires with the message, 30 days after it arrives, and the link returns `404` from then on. If the file is gone while the message is still readable, the link returns `410`. On a message sent through Bird it is the URL the sender supplied, whose availability is the sender's to guarantee.
- `sticker.mime_type` (string): Media type WhatsApp reported for the file, for example `image/jpeg`. Absent on messages sent through Bird.
- `sticker.animated` (boolean): Whether the sticker is animated. Absent on an outbound message.
- `document` (object): Document the message carried.
- `document.id` (string): ID of the stored file, to pass as `media_id` when fetching it. Absent on a message sent through Bird, whose file we never stored.
- `document.url` (string): Where to fetch the media. On an inbound message, or one from the WhatsApp Business app, this is a Bird URL you fetch with your API key. The file expires with the message, 30 days after it arrives, and the link returns `404` from then on. If the file is gone while the message is still readable, the link returns `410`. On a message sent through Bird it is the URL the sender supplied, whose availability is the sender's to guarantee.
- `document.mime_type` (string): Media type WhatsApp reported for the file, for example `image/jpeg`. Absent on messages sent through Bird.
- `document.caption` (string): Text shown beneath the document. Absent when the sender wrote none.
- `document.filename` (string): The sender's own name for the file.
- `location` (object): Location the message carried.
- `location.latitude` (number): Latitude in decimal degrees.
- `location.longitude` (number): Longitude in decimal degrees.
- `location.name` (string): Name of the place. Absent when the sender shared a plain pin.
- `location.address` (string): Street address of the place. Shown only when `name` is also set.
- `location.url` (string): Link 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.
- `contact_cards` (array of object): Contact 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.
- `contact_cards.origin` (string)

  Why 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`
- `contact_cards.vcard` (string): The 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.
- `contact_cards.name` (object): The contact's name, when the card carries one.
- `contact_cards.name.formatted_name` (string): The whole name as the contact's device renders it.
- `contact_cards.name.first_name` (string)
- `contact_cards.name.middle_name` (string)
- `contact_cards.name.last_name` (string)
- `contact_cards.name.prefix` (string)
- `contact_cards.name.suffix` (string)
- `contact_cards.org` (object): Where the contact works, when the card carries it.
- `contact_cards.org.company` (string)
- `contact_cards.org.department` (string)
- `contact_cards.org.title` (string)
- `contact_cards.birthday` (string): The 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.
- `contact_cards.phone_numbers` (array of object): The numbers on the card. A button tap carries the contact's own number here, which is the point of asking.
- `contact_cards.phone_numbers.phone_number` (string): The 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.
- `contact_cards.phone_numbers.type` (string): The 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.
- `contact_cards.emails` (array of object)
- `contact_cards.emails.email` (string)
- `contact_cards.emails.type` (string): The 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.
- `contact_cards.urls` (array of object)
- `contact_cards.urls.url` (string): The 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.
- `contact_cards.urls.type` (string): The 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.
- `contact_cards.addresses` (array of object)
- `contact_cards.addresses.street` (string)
- `contact_cards.addresses.city` (string)
- `contact_cards.addresses.state` (string)
- `contact_cards.addresses.zip` (string)
- `contact_cards.addresses.country` (string)
- `contact_cards.addresses.country_code` (string): The country as the card holds it, left exactly as WhatsApp sent it: it describes a postal address rather than a routing destination.
- `contact_cards.addresses.type` (string): The 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.
- `interactive` (object): Interactive 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.
- `interactive.type` (string, required)

  Which kind of interactive message this is, and which field carries it.

  Possible values (may grow over time): `button`, `list`, `cta_url`, `carousel`, `location_request_message`, `request_contact_info`
- `interactive.header` (object): What was shown above the body. Absent when the message carried no header.
- `interactive.header.type` (string, required)

  Which kind of header this is, and which field carries it.

  Possible values (may grow over time): `text`, `image`, `video`, `document`
- `interactive.header.text` (string): The line of text shown above the body.
- `interactive.header.url` (string): The 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.
- `interactive.body_text` (string, required): The message's main text.
- `interactive.footer_text` (string): The small print below the body. Absent when the message carried none.
- `interactive.buttons` (array of object): The buttons the message offered, in the order shown.
- `interactive.buttons.type` (string, required)

  Which kind of button this is, and which field carries it.

  Possible values (may grow over time): `quick_reply`, `cta_url`
- `interactive.buttons.quick_reply` (object): The button's label and the handle it sends back.
- `interactive.buttons.quick_reply.slug` (string, required): The 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.
- `interactive.buttons.quick_reply.text` (string, required): The label the recipient saw.
- `interactive.buttons.cta_url` (object): The button's label and the address it opens.
- `interactive.buttons.cta_url.text` (string, required): The button's label.
- `interactive.buttons.cta_url.url` (string, required): The address the button opens, as the send supplied it.
- `interactive.list` (object): The menu the message offered.
- `interactive.list.button_text` (string, required): The label of the button that opens the menu.
- `interactive.list.sections` (array of object, required): The groups of options in the menu, in the order shown.
- `interactive.list.sections.title` (string, required): The group's heading, shown above its rows.
- `interactive.list.sections.rows` (array of object, required): The options in this group, in the order shown.
- `interactive.list.sections.rows.slug` (string, required): The handle the row carries back, never shown to the recipient.
- `interactive.list.sections.rows.text` (string, required): The row's label, shown as its title in the menu.
- `interactive.list.sections.rows.description` (string): The second line under the label. Absent when the row carried none.
- `interactive.cta_url` (object): The link button the message offered.
- `interactive.cta_url.text` (string, required): The button's label.
- `interactive.cta_url.url` (string, required): The address the button opens, as the send supplied it.
- `interactive.cards` (array of object): The cards the message offered, in the order they appeared, left to right.
- `interactive.cards.header` (object, required): The image or video shown at the top of the card.
- `interactive.cards.header.type` (string, required)

  Which kind of header this is, and which field carries it.

  Possible values (may grow over time): `text`, `image`, `video`, `document`
- `interactive.cards.header.text` (string): The line of text shown above the body.
- `interactive.cards.header.url` (string): The 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.
- `interactive.cards.body_text` (string): The card's own text. Absent when the card carried none.
- `interactive.cards.buttons` (array of object, required): The buttons the card offered, in the order shown.
- `interactive.cards.buttons.type` (string, required)

  Which kind of button this is, and which field carries it.

  Possible values (may grow over time): `quick_reply`, `cta_url`
- `interactive.cards.buttons.quick_reply` (object): The button's label and the handle it sends back.
- `interactive.cards.buttons.quick_reply.slug` (string, required): The 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.
- `interactive.cards.buttons.quick_reply.text` (string, required): The label the recipient saw.
- `interactive.cards.buttons.cta_url` (object): The button's label and the address it opens.
- `interactive.cards.buttons.cta_url.text` (string, required): The button's label.
- `interactive.cards.buttons.cta_url.url` (string, required): The address the button opens, as the send supplied it.
- `in_reply_to_message_id` (string): The 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.
- `interactive_reply` (object): What the contact tapped, on a message answering an interactive message or a template's quick-reply button. Inbound only.
- `interactive_reply.type` (string, required)

  Which kind of tap this reply came from, and which field carries it.

  Possible values (may grow over time): `button`, `list`
- `interactive_reply.button` (object): The 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.
- `interactive_reply.button.slug` (string, required): The 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.
- `interactive_reply.button.text` (string, required): The label the recipient saw.
- `interactive_reply.list` (object): The row the contact chose, as you declared it. `description` is present only when the row carried one.
- `interactive_reply.list.slug` (string, required): The handle the row carries back, never shown to the recipient.
- `interactive_reply.list.text` (string, required): The row's label, shown as its title in the menu.
- `interactive_reply.list.description` (string): The second line under the label. Absent when the row carried none.
- `unsupported` (object): Set when a message that did not come from a Bird send carries content we do not model, naming the WhatsApp content type so the message is not silently empty. Never on a message sent through Bird.
- `unsupported.type` (string, required)

  The 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`, `order`, `system`, `unsupported`
- `reactions` (array of object): Emoji 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.
- `reactions.emoji` (string, required): The emoji, as WhatsApp sent it. It is not normalized, so two emoji that render identically can differ byte for byte and compare unequal.
- `reactions.from` (object, required): Who 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`.
- `reactions.from.phone_number` (string): Phone number in E.164 format, when known.
- `reactions.from.bsuid` (string): Business-scoped user ID, Meta's identifier for the WhatsApp user. Present only on the WhatsApp-user side of the message.
- `reactions.from.group_id` (string): The 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.
- `reactions.from.username` (string): Present 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.
- `reactions.from.display_name` (string): Present 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.
- `status` (string, required)

  Delivery status:

  - `accepted`: Accepted and queued for sending.
  - `sent`: Handed to the WhatsApp network.
  - `delivered`: Confirmed as delivered to the recipient's device.
  - `failed`: Permanently failed.
  - `rejected`: Refused before sending and not charged.
  - `received`: Received as an inbound message.
  - `scheduled`: Reserved and not returned.
  - `canceled`: Reserved and not returned.

  Read receipts appear in `read_at` and `whatsapp.read` events, in both
  directions: the recipient opening an outbound message, and the business
  acknowledging an inbound one.

  Possible values: `scheduled`, `accepted`, `sent`, `delivered`, `failed`, `rejected`, `canceled`, `received`
- `origin` (string)

  Where the message came from when it did not come through Bird live. Absent when your workspace sent the message through Bird, or the contact sent it to Bird.

  Possible values (may grow over time): `business_agent_echo`, `business_app_echo`, `history_sync`
- `recipient_count` (integer)

  How 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.
- `delivered_count` (integer)

  How 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.
- `read_count` (integer)

  How 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.
- `last_error` (nullable object): Failure detail for a message that did not reach the recipient. Present only when the message failed or was rejected.
- `last_error.code` (string, required)

  Standardized 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; `description` carries 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`
- `last_error.description` (string, required): Human-readable explanation of the failure.
- `last_error.meta_error_code` (nullable string): Raw error code from the WhatsApp Cloud API, when available, for low-level debugging.
- `last_error.occurred_at` (string, required): When the failure occurred.
- `created_at` (string, required): When the message was accepted for delivery.
- `sent_at` (nullable string): When the message was handed to the WhatsApp network. Null until then.
- `delivered_at` (nullable string): When delivery was confirmed. Null until then.
- `read_at` (nullable string): When 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.
- `cost` (nullable object): What 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.
- `cost.amount` (string, required): Total 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.
- `cost.currency_code` (string, required): ISO 4217 currency code. Every component is denominated in this currency.
- `cost.transaction_amount` (nullable string, required): What we charged to carry the message, as a decimal string. `null` when this component was not priced; `"0.00000"` when it priced at zero.
- `cost.passthrough_amount` (nullable string, required): Third-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.
- `tags` (array of object): Structured `{name, value}` filter labels applied to this message.
- `tags.name` (string, required): Tag name. ASCII letters, digits, underscore, and hyphen only. Case-sensitive. Maximum 32 characters.
- `tags.value` (string, required): Tag value. ASCII letters, digits, underscore, and hyphen only. Case-sensitive. Maximum 64 characters.
- `metadata` (object): Arbitrary JSON metadata stored on the message.

## Related resources

- [Should I use a Bird SDK or call the API directly?](/explained/platform/should-i-use-an-sdk-or-call-the-api-directly) (answer)
- [Build your first integration](/learn/paths/integration) (course)
- [Send your first email](/docs/get-started/send-your-first-email) (docs)

[Get an implementation brief](/learn/workspace?topic=api-basics)
