---
title: "Send a WhatsApp message"
method: POST
path: "/v1/whatsapp/messages"
tags: ["whatsapp-messages"]
---

# Send a WhatsApp message

`POST /v1/whatsapp/messages`

## Request

- Base URL: `https://us1.platform.bird.com`
- URL: `https://us1.platform.bird.com/v1/whatsapp/messages`
- Auth: one of:
  - HTTP bearer
  - API key in cookie `bird_session`

Sends one WhatsApp message to one recipient. The request carries exactly one
kind of content: a message template, or free-form `text`, `image`, `video`,
`audio`, `sticker`, `document`, `location`, `contact_cards` or
`interactive`. A request carrying none is rejected with a `422`, and one
carrying more than one is too.

A **template** is the only content WhatsApp delivers outside an open
customer service window, so it is what starts a conversation. Name the
template, optionally pick its language variant, and fill its placeholders in
`components`. A Bird-managed template selects its sender number from its
category, so the request carries no `from`; a template your workspace
authored requires one. Browse your workspace's templates in the Bird
dashboard.

**Free-form content** is deliverable only inside an open 24-hour customer
service window, which the contact opens by messaging or calling you and
resets each time they do it again. Bird tracks that window, so a send into a
closed one is refused with a `422` `WhatsAppServiceWindowClosed` before
anything is created or charged;
send a template instead, which reopens the window once the contact replies.
A window that closes between accept and dispatch still fails
asynchronously, carrying `service_window_expired` on the message's
`last_error`. Every free-form send requires `from`.

**Interactive content** gives the recipient something to tap. `interactive`
names its kind in `type` and carries that kind's own field: reply `buttons`,
a `list` menu, a `cta_url` link button, or `cards` for a carousel;
`location_request_message` and `request_contact_info` are each a single
button asking the recipient for something, so `body_text` is the whole
message. A tap on a reply button or a list row comes back as an inbound
message carrying `interactive_reply`. The other kinds answer in their own
shape: a `cta_url` link opens in the recipient's browser and sends nothing
back, and the two request kinds come back as the thing they asked for, an
inbound `location` or `contact_cards` message.
Interactive content is free-form, so the customer service window and the
`from` requirement above both apply.

**Contact cards** share up to five contacts in one message. Each card's
`name` needs `formatted_name` plus at least one other part, and a
`phone_number` in E.164 earns that card a button opening a chat with it.
Contact cards are free-form too, so the same window and `from` rules apply.

**A group send** addresses a WhatsApp group ID in `to` and carries no
`from`: the group sends on its own number, and the response reports the
fan-out.
`recipient_count` is the group's membership when the send was accepted,
`delivered_count` and `read_count` count against it, and `status` turns
`delivered` only once every participant has it. WhatsApp delivers neither
interactive content nor an authentication template to a group, and a
Bird-managed template sends from a number no group is scoped to.

Set `in_reply_to_message_id` to quote a message the contact sees above this
one, the way replying in the WhatsApp client does. Any content quotes, and
the quoted message must be one from this same conversation.

The `202` response is the accepted message, echoing the resolved content; it
is not a delivery confirmation. Follow delivery with
[Get a WhatsApp message](/docs/api/reference/get-whatsapp-message), the
per-message timeline from
[List events for a WhatsApp message](/docs/api/reference/list-whatsapp-message-events),
or `whatsapp.*` webhook events.

Each of these returns a `422`:

- A template slug or language the catalogue does not stock.
- Parameter values that do not match the template's declared placeholders.
- A `from` this workspace cannot send from.
- A recipient that is neither a valid phone number nor a business-scoped user ID.
- Free-form content sent into a closed customer service window
  (`WhatsAppServiceWindowClosed`).
- Interactive content or an authentication template addressed to a group
  (`WhatsAppGroupContentNotSupported`).

A group `to` naming no group this workspace holds fails with a `404`
`WhatsAppGroupNotFound`, and one whose group is not active with a `409`
`WhatsAppGroupNotActive`. A send from a workspace with no wallet balance
fails with a `402`.

## Headers

- `Idempotency-Key` string
- `X-Workspace-Id` string
- `X-Organization-Id` string

## Request body

- WhatsAppMessageSendRequest (unresolved $ref)

## Response `202`

Message accepted for asynchronous delivery.

- WhatsAppMessage
  - `id` string, required
  - `direction` 'outbound' | 'inbound', required: Whether the message was sent by the business (`outbound`) or received from the contact (`inbound`).
  - `from` WhatsAppAddress, required: Sender or recipient of a WhatsApp message: a phone number, a business-scoped user ID, or both. A message received from a WhatsApp user carries whatever profile they publish, which may be neither.
    - `phone_number` string: Phone number in E.164 format, when known.
    - `bsuid` string: Business-scoped user ID, Meta's identifier for the WhatsApp user. Present only on the WhatsApp-user side of the message.
    - `group_id` string
    - `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.
    - `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` WhatsAppAddress, required: Sender or recipient of a WhatsApp message: a phone number, a business-scoped user ID, or both. A message received from a WhatsApp user carries whatever profile they publish, which may be neither.
    - `phone_number` string: Phone number in E.164 format, when known.
    - `bsuid` string: Business-scoped user ID, Meta's identifier for the WhatsApp user. Present only on the WhatsApp-user side of the message.
    - `group_id` string
    - `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.
    - `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` WhatsAppMessageTemplate: The template a message was sent from. On reads `slug`, `language`, `category`, and `components` are always present; `components` is an empty array for an authentication template (the filled-in values, for example a verification code, are never returned).
    - `slug` string, required: A template's slug: what you send it by, for example `welcome-email`. Email and SMS slugs stay fixed after creation. WhatsApp slugs can change only before the first submission. A slug can contain lowercase letters, numbers, hyphens, and underscores, has to start and end with a letter or a number, and can be up to 63 characters long.
    - `category` string, required: Meta's content classification for a template. - `authentication`: delivers one-time passcodes. - `utility`: delivers transaction-triggered updates (receipts, order status). - `marketing`: carries promotional content. The category determines the sender number and price. This is an open enum. Accept unrecognized values.
    - `language` string, required: A language tag in BCP-47 form, for example `en` or `pt-BR`.
    - `components` WhatsAppMessageTemplateComponent[], required: The values that filled the template's placeholders. Empty for an authentication template, whose content is never returned.
      - `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.
      - `parameters` WhatsAppMessageTemplateComponentParameter[]: 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.
        - `type` string, required: The kind of value a template parameter carries, which follows the block it fills. The `text` type is a plain string substituted into a placeholder. This includes a coupon button's code, which the recipient copies from the button. The `image`, `video`, `gif`, and `document` types carry a media header's file in `url`. Each matches its header's `format`. The `location` type fills a location header and carries a point on the map. Open enum: more kinds may be added over time.
        - `text` string: The value substituted into the placeholder, as a plain string. Send it on a `text` parameter.
        - `url` string, uri: 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.
        - `location` WhatsAppLocationSend: A free-form location to send: a point on the map the recipient can open in their maps app. Deliverable only inside an open 24-hour customer service window; outside one, send a template instead.
          - `latitude` number, double, required: Latitude in decimal degrees.
          - `longitude` number, double, required: Longitude in decimal degrees.
          - `name` string: Name of the place, shown above the address.
          - `address` string: Street address of the place. Shown only when `name` is also set.
        - `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.
      - `cards` WhatsAppMessageTemplateCard[]: Values for each carousel card, one entry per card the template was approved with. Send it only on a `carousel` part.
        - `components` WhatsAppMessageTemplateCardComponent[], required: The values that fill this card's blocks.
          - `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.
          - `parameters` WhatsAppMessageTemplateComponentParameter[]: The values that fill this part's placeholders, in placeholder order.
            - `type` string, required: The kind of value a template parameter carries, which follows the block it fills. The `text` type is a plain string substituted into a placeholder. This includes a coupon button's code, which the recipient copies from the button. The `image`, `video`, `gif`, and `document` types carry a media header's file in `url`. Each matches its header's `format`. The `location` type fills a location header and carries a point on the map. Open enum: more kinds may be added over time.
            - `text` string: The value substituted into the placeholder, as a plain string. Send it on a `text` parameter.
            - `url` string, uri: 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.
            - `location` WhatsAppLocationSend: A free-form location to send: a point on the map the recipient can open in their maps app. Deliverable only inside an open 24-hour customer service window; outside one, send a template instead.
              - …
            - `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` WhatsAppText: Text content of a WhatsApp message.
    - `body` string, required: The message text.
  - `image` WhatsAppImage: Fields shared by every media content object on a WhatsApp message.
    - `id` string
    - `url` string, uri: 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; 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 a message sent through Bird it is the URL the sender supplied, whose availability is the sender's to guarantee.
    - `mime_type` string: Media type WhatsApp reported for the file, for example `image/jpeg`. Absent on messages sent through Bird.
    - `caption` string: Text shown beneath the image. Absent when the sender wrote none.
  - `video` WhatsAppVideo: Fields shared by every media content object on a WhatsApp message.
    - `id` string
    - `url` string, uri: 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; 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 a message sent through Bird it is the URL the sender supplied, whose availability is the sender's to guarantee.
    - `mime_type` string: Media type WhatsApp reported for the file, for example `image/jpeg`. Absent on messages sent through Bird.
    - `caption` string: Text shown beneath the video. Absent when the sender wrote none.
  - `audio` WhatsAppAudio: Fields shared by every media content object on a WhatsApp message.
    - `id` string
    - `url` string, uri: 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; 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 a message sent through Bird it is the URL the sender supplied, whose availability is the sender's to guarantee.
    - `mime_type` string: Media type WhatsApp reported for the file, for example `image/jpeg`. Absent on messages sent through Bird.
    - `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` WhatsAppSticker: Fields shared by every media content object on a WhatsApp message.
    - `id` string
    - `url` string, uri: 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; 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 a message sent through Bird it is the URL the sender supplied, whose availability is the sender's to guarantee.
    - `mime_type` string: Media type WhatsApp reported for the file, for example `image/jpeg`. Absent on messages sent through Bird.
    - `animated` boolean: Whether the sticker is animated. Absent on an outbound message.
  - `document` WhatsAppDocument: Fields shared by every media content object on a WhatsApp message.
    - `id` string
    - `url` string, uri: 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; 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 a message sent through Bird it is the URL the sender supplied, whose availability is the sender's to guarantee.
    - `mime_type` string: Media type WhatsApp reported for the file, for example `image/jpeg`. Absent on messages sent through Bird.
    - `caption` string: Text shown beneath the document. Absent when the sender wrote none.
    - `filename` string: The sender's own name for the file.
  - `location` WhatsAppLocation: Location content of a WhatsApp message: a point on the map the recipient can open in their maps app.
    - `latitude` number, double: Latitude in decimal degrees.
    - `longitude` number, double: Longitude in decimal degrees.
    - `name` string: Name of the place. Absent when the sender shared a plain pin.
    - `address` string: Street address of the place. Shown only when `name` is also set.
    - `url` string, uri: 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` WhatsAppContactCard[]: 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.
    - `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.
    - `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.
    - `name` WhatsAppContactName: The contact's name, in the parts their device supplied. Every part is optional: WhatsApp sends what the card holds and omits the rest.
      - `formatted_name` string: The whole name as the contact's device renders it.
      - `first_name` string
      - `middle_name` string
      - `last_name` string
      - `prefix` string
      - `suffix` string
    - `org` WhatsAppContactOrg: Where the contact works, as their card records it.
      - `company` string
      - `department` string
      - `title` string
    - `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.
    - `phone_numbers` WhatsAppContactPhone[]: The numbers on the card. A button tap carries the contact's own number here, which is the point of asking.
      - `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.
      - `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.
    - `emails` WhatsAppContactEmail[]
      - `email` string
      - `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.
    - `urls` WhatsAppContactUrl[]
      - `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.
      - `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.
    - `addresses` WhatsAppContactAddress[]
      - `street` string
      - `city` string
      - `state` string
      - `zip` string
      - `country` string
      - `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.
      - `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` WhatsAppInteractive: Interactive content of a WhatsApp message: body text plus something the recipient can tap. The field named by `type` is the one that is present, except on `location_request_message` and `request_contact_info`, which name no field: each is a single button asking the recipient for something, so `body_text` is the whole message. Outbound only, and so an echo of what the send asked for: a contact cannot send interactive content, and a tap on it reads as `interactive_reply`, or on a location or contact request as the message the recipient shared in answer. Unlike the send schema, this one does not pin each `type` to its field. The vocabulary in `type` is open, so dispatch on it and treat an unrecognized value as a kind added since. Only the discriminator is open: this schema declares no additional properties, so a new kind's own payload field arrives here in the same change that introduces the kind, which is additive.
    - `type` string, required: Which kind of interactive message this is. - `button`: up to three tappable buttons, each sending its own identifier back as an inbound message. Carried in `buttons`. - `list`: a single button that opens a menu of rows to choose one from. - `cta_url`: a single button that opens a link. - `carousel`: 2 to 10 media cards the recipient scrolls through sideways, each with its own buttons. Carried in `cards`. - `location_request_message`: a single button that asks the recipient to share where they are. A tap arrives as an inbound `location` message. - `request_contact_info`: a single button that asks the recipient to share their phone number. A tap arrives as an inbound message carrying the number on `contact_cards`, with `origin` set to `contact_request`. The last two name no field of their own: the ask is the whole message, and `body_text` is all they carry. Open enum: WhatsApp adds interactive kinds over time, so treat an unrecognized value as a future kind rather than an error.
    - `header` WhatsAppInteractiveHeader: What the interactive message showed above its body. `type` names the kind and the field that carries it: `text` for a line of copy, `url` for the file every other kind shows. As on the message itself, the read vocabulary is open: dispatch on `type` and treat an unrecognized value as a header kind added since.
      - `type` string, required: A header's kind, and which field carries it. Open enum: WhatsApp adds header kinds over time, so treat an unrecognized value as a future kind rather than an error.
      - `text` string: The line of text shown above the body.
      - `url` string, uri: 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.
    - `body_text` string, required: The message's main text.
    - `footer_text` string: The small print below the body. Absent when the message carried none.
    - `buttons` WhatsAppInteractiveButton[]: The buttons the message offered, in the order shown.
      - `type` string, required: Which kind of button this is, and which field carries it. - `quick_reply`: sends its own identifier back as an inbound message. - `cta_url`: opens a link in the recipient's browser. Open enum: WhatsApp adds button kinds over time, so treat an unrecognized value as a future kind rather than an error.
      - `quick_reply` WhatsAppInteractiveQuickReplyButton: A reply button's label and the handle it carries back. On the echo of a message Bird sent, the pair the send declared; on an inbound `interactive_reply`, the pair the contact tapped. No length is declared here, because a tap can echo a template's quick-reply button, whose label runs longer than an interactive message's own allows.
        - `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.
        - `text` string, required: The label the recipient saw.
      - `cta_url` WhatsAppInteractiveCtaUrl: The link button the message offered: its label and the address it opens.
        - `text` string, required: The button's label.
        - `url` string, uri, required: The address the button opens, as the send supplied it.
    - `list` WhatsAppInteractiveList: The menu the message offered: a button that opens it, and the groups of options behind it.
      - `button_text` string, required: The label of the button that opens the menu.
      - `sections` WhatsAppInteractiveListSection[], required: The groups of options in the menu, in the order shown.
        - `title` string, required: The group's heading, shown above its rows.
        - `rows` WhatsAppInteractiveListRow[], required: The options in this group, in the order shown.
          - `slug` string, required: The handle the row carries back, never shown to the recipient.
          - `text` string, required: The row's label, shown as its title in the menu.
          - `description` string: The second line under the label. Absent when the row carried none.
    - `cta_url` WhatsAppInteractiveCtaUrl: The link button the message offered: its label and the address it opens.
      - `text` string, required: The button's label.
      - `url` string, uri, required: The address the button opens, as the send supplied it.
    - `cards` WhatsAppInteractiveCard[]: The cards the message offered, in the order they appeared, left to right.
      - `header` WhatsAppInteractiveHeader, required: What the interactive message showed above its body. `type` names the kind and the field that carries it: `text` for a line of copy, `url` for the file every other kind shows. As on the message itself, the read vocabulary is open: dispatch on `type` and treat an unrecognized value as a header kind added since.
        - `type` string, required: A header's kind, and which field carries it. Open enum: WhatsApp adds header kinds over time, so treat an unrecognized value as a future kind rather than an error.
        - `text` string: The line of text shown above the body.
        - `url` string, uri: 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.
      - `body_text` string: The card's own text. Absent when the card carried none.
      - `buttons` WhatsAppInteractiveButton[], required: The buttons the card offered, in the order shown.
        - `type` string, required: Which kind of button this is, and which field carries it. - `quick_reply`: sends its own identifier back as an inbound message. - `cta_url`: opens a link in the recipient's browser. Open enum: WhatsApp adds button kinds over time, so treat an unrecognized value as a future kind rather than an error.
        - `quick_reply` WhatsAppInteractiveQuickReplyButton: A reply button's label and the handle it carries back. On the echo of a message Bird sent, the pair the send declared; on an inbound `interactive_reply`, the pair the contact tapped. No length is declared here, because a tap can echo a template's quick-reply button, whose label runs longer than an interactive message's own allows.
          - `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.
          - `text` string, required: The label the recipient saw.
        - `cta_url` WhatsAppInteractiveCtaUrl: The link button the message offered: its label and the address it opens.
          - `text` string, required: The button's label.
          - `url` string, uri, required: The address the button opens, as the send supplied it.
  - `in_reply_to_message_id` string
  - `interactive_reply` WhatsAppInteractiveReply: What the contact tapped, on an inbound message answering an interactive message or a template's quick-reply button. `type` names the kind and the field it names carries it, as everywhere else in this arm. Inbound only: a message that offers something to tap reads as `interactive` instead, and the two never appear together.
    - `type` string, required: Which kind of tap the reply came from. - `button`: a reply button on an interactive message, or a quick-reply button on a template. Both carry an identifier and a label, so they read the same way. - `list`: a row chosen from a list's menu. Only this kind carries a `description`. Open enum: WhatsApp adds interactive kinds over time, so treat an unrecognized value as a future kind rather than an error.
    - `button` WhatsAppInteractiveQuickReplyButton: A reply button's label and the handle it carries back. On the echo of a message Bird sent, the pair the send declared; on an inbound `interactive_reply`, the pair the contact tapped. No length is declared here, because a tap can echo a template's quick-reply button, whose label runs longer than an interactive message's own allows.
      - `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.
      - `text` string, required: The label the recipient saw.
    - `list` WhatsAppInteractiveListRow: One option in a list's menu. On the echo of a message Bird sent, the row as declared; on an inbound `interactive_reply`, the row the contact chose. No length is declared here: this is what WhatsApp reported, not what a send is held to.
      - `slug` string, required: The handle the row carries back, never shown to the recipient.
      - `text` string, required: The row's label, shown as its title in the menu.
      - `description` string: The second line under the label. Absent when the row carried none.
  - `unsupported` WhatsAppUnsupported: A message whose content we do not model, named so it is visible in the message log rather than arriving empty. Never on a message sent through Bird.
    - `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.
  - `reactions` WhatsAppReaction[]: 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.
    - `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.
    - `from` WhatsAppAddress, required: Sender or recipient of a WhatsApp message: a phone number, a business-scoped user ID, or both. A message received from a WhatsApp user carries whatever profile they publish, which may be neither.
      - `phone_number` string: Phone number in E.164 format, when known.
      - `bsuid` string: Business-scoped user ID, Meta's identifier for the WhatsApp user. Present only on the WhatsApp-user side of the message.
      - `group_id` string
      - `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.
      - `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` 'scheduled' | 'accepted' | 'sent' | 'delivered' | 'failed' | 'rejected' | 'canceled' | 'received', 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.
  - `origin` string: Where a message came from when it did not come through Bird live: its provenance. It does not say who answers the contact. On a number shared with the WhatsApp Business app, a message can come from the app while Bird keeps answering. - `business_agent_echo`: the Meta Business Agent on this number composed it. - `business_app_echo`: someone sent it from the WhatsApp Business app on a number shared with it. - `history_sync`: WhatsApp copied it, in either direction, from the WhatsApp Business app's chat history when the number was shared. Neither `/v1/whatsapp/messages` nor a webhook event reports an imported message, so neither carries this value.
  - `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` WhatsAppError, nullable: Failure detail for a message that could not be delivered or was rejected.
    - `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.
    - `description` string, required: Human-readable explanation of the failure.
    - `meta_error_code` string, nullable: Raw error code from the WhatsApp Cloud API, when available, for low-level debugging.
    - `occurred_at` string, date-time, required: When the failure occurred.
  - `created_at` string, date-time, required: When the message was accepted for delivery.
  - `sent_at` string, date-time, nullable: When the message was handed to the WhatsApp network. Null until then.
  - `delivered_at` string, date-time, nullable: When delivery was confirmed. Null until then.
  - `read_at` string, date-time, nullable: 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` MessageCost, nullable: What was charged for a message, split into the components that make it up. `null` until at least one component has been priced.
    - `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.
    - `currency_code` string, required: ISO 4217 three-letter currency code.
    - `transaction_amount` string, nullable, 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.
    - `passthrough_amount` string, nullable, 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` Tag[]: Structured `{name, value}` filter labels applied to this message.
    - `name` string, required: Tag name. ASCII letters, digits, underscore, and hyphen only. Case-sensitive. Maximum 32 characters.
    - `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.

## Other responses

- `400`: Bad request
- `401`: Authentication required
- `402`: Insufficient balance
- `403`: Insufficient permissions
- `404`: Resource not found
- `409`: Resource conflict
- `422`: The request has invalid field values, violates a business rule, or carries a query parameter the endpoint does not declare. Field validation errors use `type: validation_error` and include the affected fields in `details`. Business-rule errors identify the failed rule in `type`.
- `429`: Rate limit exceeded
- `500`: Internal server error
- `503`: The service is temporarily unavailable. If `Retry-After` is present, wait for that delay before retrying; otherwise, retry with exponential backoff. Reuse the same idempotency key and request when retrying a mutation.

---

[API](https://skmtc.dev/messagebird/apis/bird-api.md) · [All operations](https://skmtc.dev/messagebird/apis/bird-api/llms.txt) · [OpenAPI document](https://skmtc.dev/messagebird/apis/bird-api/revisions/6dfce1179799?raw)
