openapi: 3.2.0 info: title: Bird Whatsapp Messages API version: 1.0.0 description: 'The Bird API: one REST API for email, SMS, WhatsApp, verification, and Realtime.' servers: - url: https://{region}.platform.bird.com description: '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. ' variables: region: default: us1 enum: - us1 - eu1 description: The region your organization's data is hosted in. - url: https://platform.bird.com description: Region-independent endpoint for authentication and account administration. - url: http://localhost:8080 description: Local development. security: - BearerAuth: [] tags: - name: whatsapp-messages description: Send WhatsApp messages, whether a template, free-form content, or interactive content the recipient can tap, and read the messages your workspace sent and received, including their current delivery status and lifecycle events. paths: /v1/whatsapp/messages: get: operationId: listWhatsAppMessages x-snippet-key: whatsapp.list summary: List WhatsApp messages description: '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.' tags: - whatsapp-messages x-audiences: - public - command security: - BearerAuth: [] - CookieAuth: [] parameters: - $ref: '#/components/parameters/PaginationLimit' - $ref: '#/components/parameters/StartingAfter' - $ref: '#/components/parameters/EndingBefore' - $ref: '#/components/parameters/CreatedAfter' - $ref: '#/components/parameters/CreatedBefore' - name: status in: query required: false description: Filter by status. Repeat the parameter to match any of several statuses. schema: type: array items: $ref: '#/components/schemas/WhatsAppMessageStatus' - name: direction in: query required: false description: 'Filter by whether the business sent the message (`outbound`) or received it from the contact (`inbound`). ' schema: $ref: '#/components/schemas/MessageDirection' - name: to in: query required: false description: 'Filter 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=` matches outbound messages only. ' schema: type: string minLength: 1 example: '+15551234567' - name: from in: query required: false description: 'Filter 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=` matches inbound messages only. ' schema: type: string minLength: 1 example: '+13124495648' - name: phone_number in: query required: false deprecated: true description: 'Deprecated: 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. ' schema: type: string example: '+15551234567' - name: bsuid in: query required: false description: 'Filter 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. ' schema: type: string example: NL.xxxx - name: category in: query required: false description: Filter by category. schema: $ref: '#/components/schemas/WhatsAppTemplateCategory' - name: group_id in: query required: false description: 'Filter 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. ' schema: $ref: '#/components/schemas/WhatsAppGroupID' - $ref: '#/components/parameters/TagFilter' responses: '200': description: Paginated list of WhatsApp messages. content: application/json: schema: $ref: '#/components/schemas/WhatsAppMessageList' example: 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 '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - attio - cli - make - mcp - n8n - sdk post: operationId: createWhatsAppMessage x-snippet-key: whatsapp.send summary: Send a WhatsApp message description: Sends one WhatsApp message to one recipient. tags: - whatsapp-messages x-audiences: - public - command security: - BearerAuth: [] - CookieAuth: [] parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/WhatsAppMessageSendRequest' examples: onboarding-whatsapp: summary: The first send from the dashboard's onboarding step value: to: '+15551234567' template: slug: bird_delivery_update components: - type: body parameters: - type: text name: ref text: A1B2C3D4 - type: text name: date text: 10 Jul 2026 audio: summary: A voice note or audio clip by URL value: to: '+16505551234' from: '+13124495648' audio: url: https://cdn.example.com/vm/9f2.ogg documents: summary: A PDF or other document by URL value: to: '+16505551234' from: '+13124495648' document: url: https://cdn.example.com/invoices/a1b2c3.pdf images: summary: An image by URL value: to: '+16505551234' from: '+13124495648' image: url: https://cdn.example.com/receipt.png stickers: summary: A WebP sticker by URL value: to: '+16505551234' from: '+13124495648' sticker: url: https://cdn.example.com/stickers/thumbs-up.webp video: summary: A video by URL value: to: '+16505551234' from: '+13124495648' video: url: https://cdn.example.com/unboxing.mp4 location: summary: A pin the recipient can open in their maps app value: to: '+16505551234' from: '+13124495648' location: latitude: 37.7793 longitude: -122.4193 groups: summary: A text message to every participant of a group value: to: wag_01krdgeqcxet5s7t44vh8rt9mg text: body: The route sheet for Tuesday is up. interactive-overview: summary: Quick-reply buttons offering two replies value: to: '+15551234567' from: '+13124495648' interactive: type: button body_text: Your gardening workshop is scheduled for 9am tomorrow. buttons: - type: quick_reply quick_reply: slug: change-booking text: Change - type: quick_reply quick_reply: slug: cancel-booking text: Cancel reply-buttons: summary: A single quick-reply button value: to: '+16505551234' from: '+13124495648' interactive: type: button body_text: Your gardening workshop is scheduled for 9am tomorrow. buttons: - type: quick_reply quick_reply: slug: change-booking text: Change carousels: summary: A carousel of product cards, each with its own button value: to: '+16505551234' from: '+13124495648' interactive: type: carousel body_text: 'Here are two of our latest arrivals, each under $25:' cards: - header: type: image url: https://cdn.example.com/plants/blue-echeveria.jpeg buttons: - type: cta_url cta_url: text: Buy now url: https://shop.example.com/blue-echeveria - header: type: image url: https://cdn.example.com/plants/zebra-haworthia.jpeg buttons: - type: cta_url cta_url: text: Buy now url: https://shop.example.com/zebra-haworthia contact-info-requests: summary: A prompt asking the recipient to share their phone number value: to: '+16505551234' from: '+13124495648' interactive: type: request_contact_info body_text: To confirm your booking we need a number to reach you on. Tap below to share yours. cta-url-buttons: summary: A button that opens a URL, with click tracking in the link value: to: '+16505551234' from: '+13124495648' interactive: type: cta_url body_text: Tap the button below to see the available dates. cta_url: text: See dates url: https://example.com/workshops?click_id=a1b2c3 list-menus: summary: A list menu the recipient picks one row from value: to: '+16505551234' from: '+13124495648' interactive: type: list body_text: Which shipping option do you prefer? list: button_text: Shipping options sections: - title: As soon as possible rows: - slug: priority_express text: Priority Mail Express location-requests: summary: A prompt asking the recipient to share their location value: to: '+16505551234' from: '+13124495648' interactive: type: location_request_message body_text: Let's start with your pickup. Share your current location, or type an address instead. template-authentication: summary: An authentication template carrying a one-time passcode value: to: '+14155550100' template: slug: bird_otp language: en components: - type: body parameters: - type: text text: '481920' template-marketing: summary: A marketing template with an image header and a coupon button value: to: '+16505551234' from: '+13125550101' template: slug: summer_sale language: en components: - type: header parameters: - type: image url: https://cdn.example.com/banners/summer.png - type: body parameters: - type: text name: first_name text: Pablo - type: button parameters: - type: text text: SUMMER25 template-utility: summary: A utility template confirming an order value: to: '+16505551234' template: slug: bird_order_confirmation language: en components: - type: body parameters: - type: text name: ref text: A1B2C3D4 - type: text name: amount text: USD 49.99 plain-text: summary: A plain text message, the smallest free-form send value: to: '+16505551234' from: '+13124495648' text: body: Your driver is 2 minutes away. free-form-text: summary: A free-form reply inside an open customer service window value: to: '+31612345678' from: '+13124495648' text: body: 'Your order shipped: https://example.com/track/A1B2C3' preview_url: true interactive-buttons: summary: Reply buttons quoting the message that asked for them value: to: '+31612345678' from: '+13124495648' in_reply_to_message_id: wam_01kya19eknftrs2s6p82asmvnh interactive: type: button body_text: Your driver is 2 minutes away. Still at the same address? buttons: - type: quick_reply quick_reply: slug: same_address text: Yes, same address - type: quick_reply quick_reply: slug: change_address text: Change address responses: '202': description: Message accepted for asynchronous delivery. headers: Idempotency-Replay: $ref: '#/components/headers/IdempotencyReplay' content: application/json: schema: $ref: '#/components/schemas/WhatsAppMessage' example: id: wam_01kya19eknftrs2s6p82asmvnh direction: outbound from: phone_number: '+15550001111' to: group_id: wag_01krdgeqcxet5s7t44vh8rt9mg text: body: The route sheet for Tuesday is up. status: accepted recipient_count: 2 delivered_count: 0 read_count: 0 created_at: '2026-09-15T14:03:10Z' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '402': $ref: '#/components/responses/PaymentRequired' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - attio - cli - make - mcp - n8n - sdk /v1/whatsapp/messages/{message_id}: get: operationId: getWhatsAppMessage x-snippet-key: whatsapp.get summary: Get a WhatsApp message description: 'Returns a single WhatsApp message: its current delivery status, per-stage timestamps (`sent_at`, `delivered_at`, `read_at`), and failure detail when it failed. It 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. An inbound message whose content WhatsApp models and we do not carries `unsupported` instead, naming the type rather than reading back empty. The `status` advances asynchronously as delivery progresses, so poll this endpoint (or subscribe to `whatsapp.*` webhook events) after a send to confirm delivery. For the per-event timeline, use List events for a WhatsApp message instead.' tags: - whatsapp-messages x-audiences: - public - command security: - BearerAuth: [] - CookieAuth: [] parameters: - name: message_id in: path required: true description: ID of the message, as returned in the send response's `id` field. schema: $ref: '#/components/schemas/WhatsAppMessageID' responses: '200': description: WhatsApp message object. content: application/json: schema: $ref: '#/components/schemas/WhatsAppMessage' example: id: wam_01kya19eknftrs2s6p82asmvnh direction: outbound from: phone_number: '+15550001111' to: group_id: wag_01krdgeqcxet5s7t44vh8rt9mg text: body: Tuesday's route sheet is up. status: delivered recipient_count: 2 delivered_count: 2 read_count: 2 created_at: '2026-09-15T14:03:10Z' sent_at: '2026-09-15T14:03:11Z' delivered_at: '2026-09-15T14:03:12Z' read_at: '2026-09-15T14:04:02Z' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - attio - cli - make - mcp - n8n - sdk /v1/whatsapp/messages/{message_id}/read: post: operationId: sendWhatsAppReadReceipt summary: Mark a WhatsApp message as read description: 'Marks an inbound WhatsApp message as read, showing the contact the blue ticks. WhatsApp also marks every earlier message in that conversation read. Pass `typing_indicator: true` to show a typing indicator as well. WhatsApp clears it when you send your next message, or after 25 seconds, whichever comes first, and there is no call to clear it early, so ask for one only when you are about to reply. WhatsApp cannot show a typing indicator without a read receipt, so both arrive together. The acknowledgement is sent asynchronously: a `202` means Bird accepted the request, not that WhatsApp has shown it. Nothing reports back, because WhatsApp publishes no delivery, status or failure callback for an acknowledgement, so there is nothing to poll and no webhook event. Repeating the call is safe, and each call restarts the 25-second typing window. To refresh the indicator, send a fresh `Idempotency-Key` or none at all: a replayed key answers from the stored response without acknowledging anything again. Only an inbound message can be marked read. WhatsApp allows this for 30 days after receipt, but Bird keeps the provider id a receipt needs for 15 days, so a message older than that answers `404`. A message Bird sent, or one that never reached WhatsApp, answers `422`.' tags: - whatsapp-messages x-audiences: - public - dashboard - command x-snippet-key: whatsapp.markRead security: - BearerAuth: [] - CookieAuth: [] parameters: - name: message_id in: path required: true description: ID of the inbound message to acknowledge. schema: $ref: '#/components/schemas/WhatsAppMessageID' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: false content: application/json: schema: $ref: '#/components/schemas/WhatsAppReadReceiptRequest' responses: '202': description: Acknowledgement accepted for asynchronous delivery. headers: Idempotency-Replay: $ref: '#/components/headers/IdempotencyReplay' content: application/json: schema: $ref: '#/components/schemas/WhatsAppReadReceipt' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - sdk /v1/whatsapp/messages/{message_id}/events: get: operationId: listWhatsAppMessageEvents x-snippet-key: whatsapp.listEvents summary: List events for a WhatsApp message description: Returns a WhatsApp message's lifecycle events in chronological order, one entry per delivery transition (`whatsapp.accepted`, `whatsapp.sent`, `whatsapp.delivered`, `whatsapp.read`, `whatsapp.failed`). The timeline is bounded and returned in full, so this list is not paginated; an unknown message ID returns `404`. For the message's current state in a single field, use Get a WhatsApp message instead. tags: - whatsapp-messages x-audiences: - public - command security: - BearerAuth: [] - CookieAuth: [] parameters: - name: message_id in: path required: true description: ID of the message, as returned in the send response's `id` field. schema: $ref: '#/components/schemas/WhatsAppMessageID' - name: type in: query required: false description: 'Keep only events of this exact type (for example `whatsapp.delivered` or `whatsapp.failed`). Omit for the full timeline. ' schema: $ref: '#/components/schemas/WhatsAppEventType' responses: '200': description: Event timeline for this WhatsApp message. content: application/json: schema: $ref: '#/components/schemas/WhatsAppEventList' example: data: - id: ev_01kya19f8p2hs5w1y7k4cmqrtv type: whatsapp.sent occurred_at: '2026-09-15T14:03:11Z' - id: ev_01kya19f2m8xqe4v0t6r3bnpcd type: whatsapp.delivered occurred_at: '2026-09-15T14:03:12Z' recipient: phone_number: '+15550002222' bsuid: US.AbC1 '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - n8n - sdk /v1/whatsapp/messages/{message_id}/media/{media_id}: parameters: - name: message_id in: path required: true description: WhatsApp message ID. schema: $ref: '#/components/schemas/WhatsAppMessageID' - name: media_id in: path required: true description: Media ID, as returned in `id` on the message's content object. schema: $ref: '#/components/schemas/WhatsAppFileID' get: operationId: getWhatsAppMessageMedia summary: Get a WhatsApp message's media description: 'Redirects to a short-lived URL for the media on a received WhatsApp message. Inbound media is stored because WhatsApp''s own URL is not fetchable without our credentials; this endpoint is what the `url` on the message''s `image`, `video`, `audio`, `sticker` or `document` points at. The bytes live in object storage and are served straight from there, so they never transit the API. The response is a `302` whose `Location` is that pre-authorized storage URL, valid for 15 minutes; your client must follow redirects. The `Authorization` header must be absent from the request that fetches that URL: a client that attaches credentials centrally, at its transport, interceptor or middleware layer rather than per request, re-adds the header on every hop including the redirect, so the storage URL must be fetched with a client that carries none. Media is kept for 30 days after the message is received, and the message itself is kept longer. A message older than that still lists its media''s `mime_type` and `caption`, and this operation returns `410` once the bytes have expired. Outbound messages have no media to serve.' tags: - whatsapp-messages x-audiences: - public - command x-snippet-key: whatsapp.messages.media security: - BearerAuth: [] - CookieAuth: [] responses: '302': description: Redirect to a storage URL valid for 15 minutes. headers: Location: description: The storage URL to fetch the media from. schema: type: string format: uri '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '410': $ref: '#/components/responses/Gone' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' x-surfaces: - cli - make - mcp - sdk /v1/whatsapp/messages/{message_id}/reaction: put: operationId: upsertWhatsAppMessageReaction summary: React to a WhatsApp message description: 'Places an emoji reaction on a message this workspace received, the same way tapping and holding a message in WhatsApp does. You hold at most one reaction per message, so this replaces your existing one rather than adding another. To take a reaction back entirely, delete it. The `202` means the reaction was accepted, not that WhatsApp applied it. Reactions carry no delivery or read receipt, so the furthest one gets is sent. Read the message''s `reactions` for what currently stands, or List reaction events for a WhatsApp message for what became of each change, including one WhatsApp refused. Each of these returns a `422`: - A message this workspace sent. This endpoint places reactions on messages the contact sent; reacting to your own outbound message is not supported. - More than one emoji, since WhatsApp takes exactly one. A message Bird can no longer resolve returns a `404` instead. WhatsApp accepts a reaction on a message up to 30 days old, but Bird keeps the provider id a reaction needs for **15 days**, so that is the practical age limit. The message and its reaction log stay readable for 30; only the id a placement needs is gone. Reacting to a message received in a group addresses the group, so the group''s own state applies: a group this workspace no longer holds returns a `404` `WhatsAppGroupNotFound`, and one that is not active a `409` `WhatsAppGroupNotActive`. Reactions are not charged for.' tags: - whatsapp-messages x-audiences: - public - dashboard - command x-snippet-key: whatsapp.reaction.set security: - BearerAuth: [] - CookieAuth: [] parameters: - name: message_id in: path required: true description: 'ID of the message to react to, as returned in the `id` field of the message. Must be a message this workspace received. ' schema: $ref: '#/components/schemas/WhatsAppMessageID' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/WhatsAppReactionUpsert' examples: thumbs-up: summary: Acknowledge a message with a thumbs up value: emoji: 👍 responses: '202': description: Reaction accepted; WhatsApp applies it asynchronously. headers: Idempotency-Replay: $ref: '#/components/headers/IdempotencyReplay' content: application/json: schema: $ref: '#/components/schemas/WhatsAppReactionAccepted' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - sdk delete: operationId: deleteWhatsAppMessageReaction summary: Remove your reaction from a WhatsApp message description: 'Takes back the reaction this workspace placed on a message, the same way tapping your own reaction in WhatsApp does. Only your own reaction can be removed; one the contact placed is theirs to take back. Removing a reaction from a message you have not reacted to changes nothing and still answers `202`, so a repeated call is safe. A removal travels the same path as placing a reaction, so it is refused on the same grounds. A message this workspace sent returns a `422`: it could never have carried a reaction of ours to remove, since placing one there is not supported either. A message Bird can no longer resolve returns a `404` instead, on the same 15-day retention a placement is bounded by. A reaction on a message received in a group addresses the group, so a group this workspace no longer holds returns a `404` `WhatsAppGroupNotFound` and one that is not active a `409` `WhatsAppGroupNotActive`, the same as placing one does. The `202` is the removal accepted rather than applied. The reaction stays in the message''s `reactions` until WhatsApp confirms the removal and then drops out, so one on its way off reads as still standing rather than disappearing before it is gone.' tags: - whatsapp-messages x-audiences: - public - dashboard - command x-snippet-key: whatsapp.reaction.remove security: - BearerAuth: [] - CookieAuth: [] parameters: - name: message_id in: path required: true description: 'ID of the message to take your reaction off, as returned in the `id` field of the message. ' schema: $ref: '#/components/schemas/WhatsAppMessageID' - $ref: '#/components/parameters/IdempotencyKey' responses: '202': description: 'Removal accepted. No body: the reaction is still standing at this point, and on a message this workspace never reacted to there is nothing to return. Read the message''s `reactions` to see it go. ' headers: Idempotency-Replay: $ref: '#/components/headers/IdempotencyReplay' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - sdk /v1/whatsapp/messages/{message_id}/reaction-events: get: operationId: listWhatsAppMessageReactionEvents summary: List reaction events for a WhatsApp message description: 'Returns the changes made to this message''s reactions as a cursor-paginated list, newest first: each emoji placed, each one replaced by a different emoji, and each one taken back. Entries are never edited, so a contact who reacts, changes their mind and then removes it leaves three of them. One case is missing rather than recorded. A reaction is matched to the message it was placed on through a provider id we keep for 15 days, while WhatsApp accepts a reaction on a message up to 30 days old, so one placed on a message older than that cannot be matched and is recorded nowhere: not here, and not in the message''s `reactions`. Use this to show who reacted and when, or to find out what became of a reaction that never appeared: a `failed` or `rejected` entry carries the reason on `error`. For what currently stands on the message, read its `reactions`, which folds this log down to one entry per sender. Pass the response''s `next_cursor` back as `starting_after` to fetch the next page. Reaction events are kept for **30 days**, counted from when the message they belong to was accepted rather than from the reaction itself. They therefore go at about the same time as the message, not 30 days after the last reaction on it.' tags: - whatsapp-messages x-audiences: - public - dashboard - command x-snippet-key: whatsapp.reaction.listEvents security: - BearerAuth: [] - CookieAuth: [] parameters: - name: message_id in: path required: true description: 'ID of the message whose reactions to read, as returned in the `id` field of the message. ' schema: $ref: '#/components/schemas/WhatsAppMessageID' - $ref: '#/components/parameters/PaginationLimit' - $ref: '#/components/parameters/StartingAfter' - $ref: '#/components/parameters/EndingBefore' responses: '200': description: Paginated list of reaction changes for this WhatsApp message. content: application/json: schema: $ref: '#/components/schemas/WhatsAppReactionEventList' example: data: - id: war_01krdgeqcxet5s7t44vh8rt9mh emoji: 🎉 status: rejected from: phone_number: '+13124495569' error: code: internal_error description: the receiving number is no longer connected occurred_at: '2026-08-28T19:04:22Z' occurred_at: '2026-08-28T19:04:22Z' - id: war_01krdgeqcxet5s7t44vh8rt9mg emoji: 👍 status: received from: phone_number: '+14155550100' bsuid: US.13491208655302741918 occurred_at: '2026-08-28T19:01:10Z' next_cursor: null prev_cursor: null refresh_cursor: eyJ2IjoxLCJzIjoiMjAyNi0wOC0yOFQxOTowNDoyMloiLCJpIjoiMDE5ZTFiMDctNWQ5ZC03NjhiLTkzZTgtODRkYzUxOGQyNjkxIn0 '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - sdk components: schemas: WhatsAppContactCard: type: object additionalProperties: false description: 'A contact card on this message: one the contact shared, or one this workspace sent. Nothing here is required. WhatsApp sends the parts the card holds and omits the rest, and a card that arrives with only an `origin` is still meaningful, so an empty card reads back empty rather than being dropped. ' properties: origin: type: string minLength: 1 x-extensible-enum: - contact_request - other description: '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. ' example: contact_request vcard: type: string description: '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. ' example: 'BEGIN:VCARD VERSION:3.0 N:Johnson;Barbara;;; TEL;type=CELL:+16505551234 END:VCARD ' name: allOf: - $ref: '#/components/schemas/WhatsAppContactName' description: The contact's name, when the card carries one. org: allOf: - $ref: '#/components/schemas/WhatsAppContactOrg' description: Where the contact works, when the card carries it. birthday: type: string description: '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. ' example: '1999-01-23' phone_numbers: type: array description: 'The numbers on the card. A button tap carries the contact''s own number here, which is the point of asking. ' items: $ref: '#/components/schemas/WhatsAppContactPhone' emails: type: array items: $ref: '#/components/schemas/WhatsAppContactEmail' urls: type: array items: $ref: '#/components/schemas/WhatsAppContactUrl' addresses: type: array items: $ref: '#/components/schemas/WhatsAppContactAddress' example: origin: contact_request phone_numbers: - phone_number: '+16505551234' type: CELL WhatsAppErrorCode: type: string minLength: 1 x-extensible-enum: - insufficient_balance - price_not_found - internal_error - undeliverable - service_window_expired - rate_limited - recipient_suppressed - media_rejected description: '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. ' WhatsAppInteractiveCard: type: object additionalProperties: false required: - header - buttons description: One card the carousel showed, in the position it appeared in. properties: header: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveHeader' description: The image or video shown at the top of the card. body_text: type: string description: The card's own text. Absent when the card carried none. example: '*Blue Echeveria*' buttons: type: array description: The buttons the card offered, in the order shown. items: $ref: '#/components/schemas/WhatsAppInteractiveButton' CurrencyCode: type: string minLength: 3 maxLength: 3 pattern: ^[A-Z]{3}$ description: ISO 4217 three-letter currency code. example: EUR WhatsAppReadReceipt: type: object additionalProperties: false required: - typing_indicator description: 'The acknowledgement Bird accepted. There is no status to poll afterwards: WhatsApp reports nothing about a read receipt. ' properties: typing_indicator: type: boolean description: Whether a typing indicator was requested alongside the read receipt. example: true WhatsAppInteractiveButtonTypeWrite: type: string minLength: 1 enum: - quick_reply - cta_url x-enum-varnames: - WhatsAppInteractiveButtonTypeWriteQuickReply - WhatsAppInteractiveButtonTypeWriteCtaUrl description: "Which kind of button this is, and which field carries it.\n\n- `quick_reply`: sends its own identifier back as an inbound message. The\n name `WhatsAppTemplateButtonTypeWrite` already uses for the same control.\n- `cta_url`: opens a link in the recipient's browser.\n\nClosed on the write side: a kind Bird cannot send to Meta is rejected rather\nthan accepted and then failed asynchronously.\n" example: quick_reply LanguageTag: type: string minLength: 2 maxLength: 35 description: A language tag in BCP-47 form, for example `en` or `pt-BR`. example: pt-BR WhatsAppTemplateID: type: string minLength: 1 pattern: ^wat_[0-9a-hjkmnp-tv-z]{26}$ example: wat_01krdgeqcxet5s7t44vh8rt9mg WhatsAppInteractiveCardSend: type: object additionalProperties: false required: - header - buttons description: 'One card in a carousel: media at the top, optional text of its own, and the buttons under it. A card has no footer, and its position in `cards` is the position it appears in. ' oneOf: - properties: buttons: maxItems: 1 items: properties: type: const: cta_url - properties: buttons: items: properties: type: const: quick_reply properties: header: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveCardHeaderSend' description: The image or video at the top of the card. body_text: type: string minLength: 1 maxLength: 160 pattern: ^[^\n]*(\n[^\n]*){0,2}$ description: 'The card''s own text, below its media, with at most two line breaks. Optional: a card can carry media and buttons alone. ' example: '*Blue Echeveria* A rosette-shaped succulent with powdery blue leaves.' buttons: type: array minItems: 1 maxItems: 3 description: 'The buttons under the card, in the order given. Either one `cta_url` button or up to three `quick_reply` buttons: the two kinds cannot be mixed on one card. Every card in the carousel must carry the same kinds in the same number, and a carousel whose cards disagree returns a `422` `WhatsAppInteractiveCarouselButtonsMismatch`. ' items: $ref: '#/components/schemas/WhatsAppInteractiveButtonSend' WhatsAppMessageID: type: string minLength: 1 pattern: ^wam_[0-9a-hjkmnp-tv-z]{26}$ example: wam_01krdgeqcxet5s7t44vh8rt9mg WhatsAppReadReceiptRequest: type: object additionalProperties: false description: 'What to acknowledge on the inbound message. An absent body and `{}` mean the same thing: mark the message read and show nothing. ' properties: typing_indicator: type: boolean default: false example: true description: 'Show a typing indicator to the contact as well as marking the message read. WhatsApp clears it when you send your next message, or after 25 seconds, whichever comes first. Only ask for one if you are about to reply. ' WhatsAppTemplateParameterType: type: string minLength: 1 x-extensible-enum: - text - image - video - gif - document - location description: '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. ' WhatsAppInteractiveQuickReplyButtonSend: type: object additionalProperties: false required: - slug - text description: 'One tappable reply button. Tapping it sends `slug` back as an inbound message, which reads as an `interactive_reply`. ' properties: slug: type: string minLength: 1 maxLength: 256 description: 'Your own handle for this button, echoed back on the reply. You choose the value and it is never shown to the recipient, so it can carry whatever your application needs to route the answer. Any characters, up to 256. ' example: change-booking text: type: string minLength: 1 maxLength: 20 description: 'The button''s label. It must differ from every other button''s label in the same message, because the recipient''s reply is identified to them by the label they tapped. ' example: Change example: slug: change-booking text: Change WhatsAppInteractiveSend: type: object additionalProperties: false required: - type - body_text description: 'An interactive message to send: body text plus something for the recipient to tap. Name the kind in `type` and carry that kind''s field alongside it: `buttons`, `list`, `cta_url` or `cards`. The schema pins each `type` to its own field and bars the other kinds'', so a request carrying a second kind''s field alongside the right one is refused. `location_request_message` and `request_contact_info` name no field: each is a single button asking the recipient for something, so `body_text` is the whole message and every other kind''s field is barred. Deliverable only inside an open 24-hour customer service window; outside one, send a template instead. ' oneOf: - properties: type: const: button body_text: maxLength: 1024 buttons: items: properties: type: const: quick_reply list: not: {} cta_url: not: {} cards: not: {} required: - type - buttons - properties: type: const: list header: properties: type: const: text url: not: {} buttons: not: {} cta_url: not: {} cards: not: {} required: - type - list - properties: type: const: cta_url body_text: maxLength: 1024 buttons: not: {} list: not: {} cards: not: {} required: - type - cta_url - properties: type: const: carousel body_text: maxLength: 1024 header: not: {} footer_text: not: {} buttons: not: {} list: not: {} cta_url: not: {} required: - type - cards - properties: type: const: location_request_message body_text: maxLength: 1024 header: not: {} footer_text: not: {} buttons: not: {} list: not: {} cta_url: not: {} cards: not: {} required: - type - properties: type: const: request_contact_info body_text: maxLength: 1024 header: not: {} footer_text: not: {} buttons: not: {} list: not: {} cta_url: not: {} cards: not: {} required: - type properties: type: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveTypeWrite' description: Which kind of interactive message this is, and which field carries it. header: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveHeaderSend' description: 'Optional content above the body. A `list` accepts a `text` header only; `button` and `cta_url` also accept an image, video or document. A `carousel` accepts none: its cards carry their own media. Neither request kind accepts one. ' body_text: type: string minLength: 1 maxLength: 4096 description: 'The message''s main text, required on every kind, and the whole message on `location_request_message` and `request_contact_info`. The WhatsApp client turns any URL it contains into a clickable link. Only a `list` may use the full length; the other kinds cap it at 1024 characters. ' example: Your workshop is scheduled for 9am tomorrow. footer_text: type: string minLength: 1 maxLength: 60 description: 'Optional small print below the body and above the buttons. A `carousel` and both request kinds take no footer. ' example: Dates are subject to change. buttons: type: array minItems: 1 maxItems: 3 description: 'The buttons to show, in the order given. Send this on a `button` message, where every button is a `quick_reply`. Every label must be unique within the message; a repeat returns a `422` `WhatsAppInteractiveDuplicateLabel`. ' items: $ref: '#/components/schemas/WhatsAppInteractiveButtonSend' list: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveListSend' description: The menu to show. Send this on a `list` message. cta_url: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveCtaUrlSend' description: The link button to show. Send this on a `cta_url` message. cards: type: array minItems: 2 maxItems: 10 description: 'The cards to show, in the order they appear, left to right. Send this on a `carousel` message, with between 2 and 10 cards. The message''s own `body_text` introduces them; a carousel carries no header and no footer of its own. ' items: $ref: '#/components/schemas/WhatsAppInteractiveCardSend' examples: - type: button header: type: image url: https://cdn.example.com/banners/workshop.png body_text: Your gardening workshop is scheduled for 9am tomorrow. Use the buttons if you need to reschedule. footer_text: Lucky Shrub, your gateway to succulents buttons: - type: quick_reply quick_reply: slug: change-booking text: Change - type: quick_reply quick_reply: slug: cancel-booking text: Cancel - type: list header: type: text text: Choose a shipping option body_text: Which shipping option do you prefer? list: button_text: Shipping options sections: - title: As soon as possible rows: - slug: priority_express text: Priority Mail Express description: Next day to 2 days - title: I can wait a bit rows: - slug: ground_advantage text: Ground Advantage description: 2 to 5 days - type: cta_url body_text: Tap the button below to see the available dates. footer_text: Dates are subject to change. cta_url: text: See dates url: https://example.com/workshops?click_id=a1b2c3 - type: carousel body_text: 'Here are two of our latest arrivals, each under $25:' cards: - header: type: image url: https://cdn.example.com/plants/blue-echeveria.jpeg body_text: Blue Echeveria. A rosette-shaped succulent with powdery blue leaves. buttons: - type: cta_url cta_url: text: Buy now url: https://shop.example.com/blue-echeveria - header: type: image url: https://cdn.example.com/plants/zebra-haworthia.jpeg body_text: Zebra Haworthia. Striking white stripes on deep green leaves. buttons: - type: cta_url cta_url: text: Buy now url: https://shop.example.com/zebra-haworthia - type: location_request_message body_text: Let's start with your pickup. Share your current location, or type an address instead. - type: request_contact_info body_text: To confirm your booking we need a number to reach you on. Tap below to share yours. TemplateSlug: type: string minLength: 1 maxLength: 63 pattern: ^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$ description: '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. ' example: welcome-email WhatsAppContactUrlSend: type: object additionalProperties: false description: One website to put on a contact card. required: - url properties: url: type: string minLength: 1 maxLength: 2048 description: 'The address to show. Not validated as a URL, because a card commonly carries a bare domain. ' example: https://luckyshrub.example.com type: type: string maxLength: 64 description: 'A label for the website, shown beside it. Free text, sent exactly as written. ' example: Company WhatsAppReactionEventList: allOf: - type: object required: - data properties: data: type: array description: Changes to this message's reactions, newest first. items: $ref: '#/components/schemas/WhatsAppReactionEvent' - $ref: '#/components/schemas/_ListEnvelope' WhatsAppInteractiveListSection: type: object additionalProperties: false required: - title - rows description: One group of options in the menu the message showed. properties: title: type: string minLength: 1 description: The group's heading, shown above its rows. example: As soon as possible rows: type: array description: The options in this group, in the order shown. items: $ref: '#/components/schemas/WhatsAppInteractiveListRow' WhatsAppMessageTemplateCard: type: object additionalProperties: false required: - components description: 'The values that fill one card of a carousel. Cards fill in the order the template was approved with, so send one entry per card and keep them in that order. ' properties: components: type: array description: The values that fill this card's blocks. items: $ref: '#/components/schemas/WhatsAppMessageTemplateCardComponent' WhatsAppInteractiveCtaUrl: type: object additionalProperties: false required: - text - url description: 'The link button the message offered: its label and the address it opens. ' properties: text: type: string minLength: 1 description: The button's label. example: See dates url: type: string format: uri minLength: 1 description: The address the button opens, as the send supplied it. example: https://example.com/workshops?click_id=a1b2c3 WhatsAppInteractiveHeader: type: object additionalProperties: false required: - type description: '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. ' properties: type: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveHeaderType' description: Which kind of header this is, and which field carries it. text: type: string description: The line of text shown above the body. example: New workshop dates announced url: type: string format: uri description: '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. ' example: https://cdn.example.com/banners/workshop.png WhatsAppVideo: type: object description: 'Video content of a WhatsApp message. ' allOf: - $ref: '#/components/schemas/WhatsAppMedia' - type: object properties: caption: type: string description: Text shown beneath the video. Absent when the sender wrote none. example: How to set it up Tag: type: object additionalProperties: false required: - name - value description: 'Structured key/value label attached to a message or a call. Use tags for low-cardinality filtering dimensions (category, experiment ID, template ID); they surface in the list filter of whatever carries them. On a message they also surface in the event log and in webhook payloads, and a message can carry `metadata` beside them for arbitrary per-send context that does not need to be filterable. A call has none of those three: its tags are set on the wire when the call is placed, and the call record is the one place you read them back. Whatever carries the tags defines how many it may have. Tag names are unique: a send that repeats one is rejected, and on a call the first instance of a name wins. ' properties: name: type: string minLength: 1 maxLength: 32 pattern: ^[A-Za-z0-9_-]+$ description: 'Tag name. ASCII letters, digits, underscore, and hyphen only. Case-sensitive. Maximum 32 characters. ' example: category value: type: string minLength: 1 maxLength: 64 pattern: ^[A-Za-z0-9_-]+$ description: 'Tag value. ASCII letters, digits, underscore, and hyphen only. Case-sensitive. Maximum 64 characters. ' example: welcome WhatsAppAudioSend: type: object additionalProperties: false required: - url description: 'Free-form audio to send, either as a voice note or as a basic audio file. Deliverable only inside an open 24-hour customer service window; outside one, send a template instead. ' properties: url: type: string format: uri minLength: 1 description: 'Public `https` URL of the audio file. WhatsApp fetches it at send time, so it must still be reachable then: a signed URL has to outlive the send. We do not store or proxy the file. WhatsApp caches a fetched URL for 10 minutes and re-serves that copy for an identical URL sent again within the window; vary the URL to force a re-fetch. AAC, AMR, MP3, M4A and OGG (OPUS codec, mono) are supported, up to 16 MB. ' example: https://cdn.example.com/voice/9f2e4a.ogg voice: type: boolean default: false description: 'Whether to send this as a voice note rather than a basic audio message. A voice note auto-downloads, shows the sender''s profile picture, and can be transcribed for the recipient. It requires an `.ogg` file encoded with the OPUS codec; any other format makes transcription fail. Leave it false for an ordinary audio attachment. ' example: url: https://cdn.example.com/voice/9f2e4a.ogg voice: true WhatsAppDocumentSend: type: object additionalProperties: false required: - url description: 'A free-form document to send, with an optional caption and filename. Deliverable only inside an open 24-hour customer service window; outside one, send a template instead. ' properties: url: type: string format: uri minLength: 1 description: 'Public `https` URL of the document. WhatsApp fetches it at send time, so it must still be reachable then: a signed URL has to outlive the send. We do not store or proxy the file. WhatsApp caches a fetched URL for 10 minutes and re-serves that copy for an identical URL sent again within the window; vary the URL to force a re-fetch. Up to 100 MB. PDF, Word, Excel, PowerPoint and plain text render reliably in the WhatsApp client; other file types are transmitted but WhatsApp does not support them. ' example: https://cdn.example.com/invoices/a1b2c3.pdf caption: type: string maxLength: 1024 description: Text shown beneath the document. example: Your invoice for order A1B2C3 filename: type: string minLength: 1 maxLength: 100 description: 'Name the recipient sees, including the extension. WhatsApp derives one from the URL when you omit it. ' example: invoice-a1b2c3.pdf example: url: https://cdn.example.com/invoices/a1b2c3.pdf caption: Your invoice for order A1B2C3 filename: invoice-a1b2c3.pdf WhatsAppStickerSend: type: object additionalProperties: false required: - url description: 'A free-form sticker to send. Deliverable only inside an open 24-hour customer service window; outside one, send a template instead. ' properties: url: type: string format: uri minLength: 1 description: 'Public `https` URL of the sticker. WhatsApp fetches it at send time, so it must still be reachable then: a signed URL has to outlive the send. We do not store or proxy the file. WhatsApp caches a fetched URL for 10 minutes and re-serves that copy for an identical URL sent again within the window; vary the URL to force a re-fetch. WebP only: up to 100 KB for a static sticker and 500 KB for an animated one. A sticker carries no caption. ' example: https://cdn.example.com/stickers/thumbs-up.webp example: url: https://cdn.example.com/stickers/thumbs-up.webp WhatsAppMedia: type: object description: Fields shared by every media content object on a WhatsApp message. properties: id: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppFileID' description: 'ID of the stored file, to pass as `media_id` when fetching it. Absent on an outbound message, whose file we never stored. ' url: type: string format: uri description: 'Where 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. ' example: https://platform.bird.com/v1/whatsapp/messages/wam_01kya19eknftrs2s6p82asmvnh/media/waf_01kyb2m4xq7whs0d8n3prv6tez mime_type: type: string description: 'Media type WhatsApp reported for the file, for example `image/jpeg`. Absent on outbound messages. ' example: image/jpeg WhatsAppInteractiveReply: type: object additionalProperties: false description: '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. ' required: - type properties: type: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveReplyType' description: Which kind of tap this reply came from, and which field carries it. button: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveQuickReplyButton' description: '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. ' list: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveListRow' description: 'The row the contact chose, as you declared it. `description` is present only when the row carried one. ' example: type: list list: slug: priority_express text: Priority Mail Express description: Next day to 2 days WhatsAppContactNameSend: type: object additionalProperties: false description: 'The contact''s name. `formatted_name` is what the card shows, and WhatsApp additionally requires at least one of the parts below it, so a card carrying only a formatted name is rejected. ' required: - formatted_name properties: formatted_name: type: string minLength: 1 maxLength: 256 description: The whole name, as the card should render it. example: Barbara J. Johnson first_name: type: string maxLength: 256 example: Barbara middle_name: type: string maxLength: 256 example: Joana last_name: type: string maxLength: 256 example: Johnson prefix: type: string maxLength: 64 example: Dr. suffix: type: string maxLength: 64 example: Esq. WhatsAppInteractiveButtonType: type: string minLength: 1 x-extensible-enum: - quick_reply - cta_url description: '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. ' example: quick_reply WhatsAppDocument: type: object description: 'Document content of a WhatsApp message. ' allOf: - $ref: '#/components/schemas/WhatsAppMedia' - type: object properties: caption: type: string description: Text shown beneath the document. Absent when the sender wrote none. example: Signed contract filename: type: string description: The sender's own name for the file. example: contract-a1b2c3.pdf WhatsAppInteractiveQuickReplyButton: type: object additionalProperties: false required: - slug - text description: '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. ' properties: slug: type: string minLength: 1 description: '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. ' example: change-booking text: type: string minLength: 1 description: The label the recipient saw. example: Change WhatsAppEventList: type: object additionalProperties: false required: - data properties: data: type: array description: Timeline events for this WhatsApp message, in chronological order. The timeline is bounded and returned in full; this list is not paginated. items: $ref: '#/components/schemas/WhatsAppEvent' WhatsAppTextSend: type: object additionalProperties: false required: - body description: 'Free-form text to send. Deliverable only inside an open 24-hour customer service window; outside one, send a template instead. ' properties: body: type: string minLength: 1 maxLength: 4096 description: 'The message text. The WhatsApp client turns any URL it contains into a clickable link. ' example: Your driver is 2 minutes away. preview_url: type: boolean default: false description: 'Whether the WhatsApp client renders a preview of the first URL in `body`. A URL must begin with `http://` or `https://`, only the first one is previewed, and the client falls back to a plain link when it cannot fetch a preview. Not returned when the message is read back, because WhatsApp does not report whether a preview rendered. ' example: body: 'Your order shipped: https://example.com/track/A1B2C3' preview_url: true WhatsAppInteractiveCardHeaderSend: type: object additionalProperties: false required: - type - url description: 'The media at the top of a carousel card. Every card must carry one, and it must be an image or a video: a card takes no text or document header. ' oneOf: - properties: type: const: image - properties: type: const: video properties: type: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveHeaderTypeWrite' description: 'Which kind of media this is. A card accepts `image` or `video` only. ' url: type: string format: uri minLength: 1 description: 'Public `https` URL of the file to show at the top of the card. An image must be JPEG or PNG, up to 5 MB; a video, MP4 with H.264 video and AAC audio, up to 16 MB. WhatsApp fetches it at send time, on the same terms as a message header''s `url`. ' example: https://cdn.example.com/plants/blue-echeveria.jpeg example: type: image url: https://cdn.example.com/plants/blue-echeveria.jpeg WhatsAppMessageList: allOf: - type: object required: - data properties: data: type: array description: Page of WhatsApp messages, newest first. items: $ref: '#/components/schemas/WhatsAppMessage' - $ref: '#/components/schemas/_ListEnvelope' WhatsAppContactName: type: object additionalProperties: false description: 'The contact''s name, in the parts their device supplied. Every part is optional: WhatsApp sends what the card holds and omits the rest. ' properties: formatted_name: type: string description: The whole name as the contact's device renders it. example: Barbara J. Johnson first_name: type: string example: Barbara middle_name: type: string example: Joana last_name: type: string example: Johnson prefix: type: string example: Dr. suffix: type: string example: Esq. WhatsAppInteractiveCtaUrlSend: type: object additionalProperties: false required: - text - url description: 'A button that opens a link in the recipient''s browser, so a long or opaque address never has to appear in the message body. ' properties: text: type: string minLength: 1 maxLength: 20 description: The button's label. example: See dates url: type: string format: uri minLength: 1 maxLength: 2000 description: 'The address the button opens. It is fixed for every recipient, so per recipient tracking belongs in the address you supply, for example as a query parameter you generate per send. ' example: https://example.com/workshops?click_id=a1b2c3 example: text: See dates url: https://example.com/workshops?click_id=a1b2c3 _ListEnvelope: type: object required: - next_cursor - prev_cursor - refresh_cursor properties: next_cursor: type: - string - 'null' description: Cursor for the next page. Pass back as `starting_after` to advance forward. `null` when no next page exists. example: eyJ2IjoxLCJzIjoiXCIyMDI2LTA1LTI1VDE0OjAzOjEwWlwiIiwiaSI6IjAxOTJmM2IxLTRjN2UtN2EyYi05ZDYxLThmM2E1YzJlN2I0MCJ9 prev_cursor: type: - string - 'null' description: Cursor for the previous page. Pass back as `ending_before` to step backward. `null` when no previous page exists. example: null refresh_cursor: type: - string - 'null' description: Refresh 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`. example: eyJ2IjoxLCJzIjoiXCIyMDI2LTA1LTI1VDE2OjQyOjAxWlwiIiwiaSI6IjAxOTJmM2IxLTllMDQtN2NkMy1iODE3LTJhNmY0ZDFjOGUwOSJ9 WhatsAppInteractiveListRow: type: object additionalProperties: false required: - slug - text description: '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. ' properties: slug: type: string minLength: 1 description: The handle the row carries back, never shown to the recipient. example: priority_express text: type: string minLength: 1 description: The row's label, shown as its title in the menu. example: Priority Mail Express description: type: string description: The second line under the label. Absent when the row carried none. example: Next day to 2 days WhatsAppAddress: type: object additionalProperties: false description: '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.' properties: phone_number: type: string minLength: 1 description: Phone number in E.164 format, when known. example: '+15550001111' bsuid: type: string minLength: 1 description: 'Business-scoped user ID, Meta''s identifier for the WhatsApp user. Present only on the WhatsApp-user side of the message. ' example: NL.xxxx group_id: allOf: - $ref: '#/components/schemas/WhatsAppGroupID' description: '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. ' username: type: string minLength: 1 description: '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: type: string minLength: 1 description: '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. ' WhatsAppImageSend: type: object additionalProperties: false required: - url description: 'A free-form image to send, with an optional caption. Deliverable only inside an open 24-hour customer service window; outside one, send a template instead. ' properties: url: type: string format: uri minLength: 1 description: 'Public `https` URL of the image. WhatsApp fetches it at send time, so it must still be reachable then: a signed URL has to outlive the send. We do not store or proxy the file. WhatsApp caches a fetched URL for 10 minutes and re-serves that copy for an identical URL sent again within the window; vary the URL to force a re-fetch. JPEG and PNG only, up to 5 MB. ' example: https://cdn.example.com/receipts/a1b2c3.png caption: type: string maxLength: 1024 description: Text shown beneath the image. example: Your receipt for order A1B2C3 example: url: https://cdn.example.com/receipts/a1b2c3.png caption: Your receipt for order A1B2C3 WhatsAppUnsupported: type: object additionalProperties: false description: 'A message whose content we do not model, named so it is visible in the message log rather than arriving empty. Inbound only. ' required: - type properties: type: type: string minLength: 1 x-extensible-enum: - reaction - interactive - button - order - system - unsupported description: '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. ' example: reaction example: type: reaction WhatsAppError: type: - object - 'null' additionalProperties: false readOnly: true required: - code - description - occurred_at description: Failure detail for a message that could not be delivered or was rejected. properties: code: $ref: '#/components/schemas/WhatsAppErrorCode' description: type: string minLength: 1 readOnly: true description: Human-readable explanation of the failure. example: Message could not be delivered. meta_error_code: type: - string - 'null' readOnly: true description: Raw error code from the WhatsApp Cloud API, when available, for low-level debugging. example: '131026' occurred_at: type: string format: date-time minLength: 1 readOnly: true description: When the failure occurred. WhatsAppReactionAccepted: type: object additionalProperties: false readOnly: true required: - id - emoji description: 'A reaction as accepted, which WhatsApp has not applied yet and may still refuse. It names the reaction-log entry the request created, so a caller that places two changes on one message can tell which entry is which; read the message''s `reactions` for what currently stands, or its reaction log for what became of this one. ' properties: id: $ref: '#/components/schemas/WhatsAppReactionEventID' description: 'ID of the reaction-log entry this request created, matching the `id` that entry carries in [List reaction events for a WhatsApp message](/docs/api/reference/list-whatsapp-message-reaction-events). ' emoji: type: string minLength: 1 description: The emoji as accepted, echoing the one the request carried. example: 👍 WhatsAppInteractiveListRowSend: type: object additionalProperties: false required: - slug - text description: 'One option in a list''s menu. Choosing it sends `slug` back as an inbound message, which reads as an `interactive_reply`. ' properties: slug: type: string minLength: 1 maxLength: 200 description: 'Your own handle for this option, echoed back on the reply. You choose the value and it is never shown to the recipient. Any characters, up to 200. ' example: priority_express text: type: string minLength: 1 maxLength: 24 description: 'The option''s label, shown as the row''s title in the menu. It must differ from every other row''s label and from every button''s label in the same message, not merely within its own group; a repeat returns a `422` `WhatsAppInteractiveDuplicateLabel`. ' example: Priority Mail Express description: type: string maxLength: 72 description: A second line under the label, for detail that will not fit in it. example: Next day to 2 days example: slug: priority_express text: Priority Mail Express description: Next day to 2 days WhatsAppInteractiveHeaderSend: type: object additionalProperties: false required: - type description: 'What to show above an interactive message''s 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, the same pairing a template''s `parameters` use. A `list` accepts a `text` header only, and any other kind on it is refused. ' oneOf: - properties: type: const: text url: not: {} required: - type - text - properties: type: enum: - image - video - document text: not: {} required: - type - url properties: type: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveHeaderTypeWrite' description: Which kind of header this is, and which field carries it. text: type: string minLength: 1 maxLength: 60 description: A single line of text above the body. Send it on a `text` header. example: New workshop dates announced url: type: string format: uri minLength: 1 description: 'Public `https` URL of the file to show. Send it on an `image`, `video` or `document` header. An image must be JPEG or PNG, up to 5 MB; a video, MP4 with H.264 video and AAC audio, up to 16 MB; a document, up to 100 MB, and PDF, Word, Excel, PowerPoint and plain text render reliably in the WhatsApp client while other file types are transmitted but unsupported. WhatsApp fetches it at send time, so it must still be reachable then: a signed URL has to outlive the send. We do not store or proxy the file. WhatsApp caches a fetched URL for 10 minutes and re-serves that copy for an identical URL sent again within the window; vary the URL to force a re-fetch. ' example: https://cdn.example.com/banners/workshop.png example: type: image url: https://cdn.example.com/banners/workshop.png MessageCost: type: - object - 'null' additionalProperties: false required: - amount - currency_code - transaction_amount - passthrough_amount description: 'What was charged for a message, split into the components that make it up. `null` until at least one component has been priced. ' properties: amount: type: string minLength: 1 readOnly: true description: '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. ' example: '0.00990' currency_code: readOnly: true $ref: '#/components/schemas/CurrencyCode' description: ISO 4217 currency code. Every component is denominated in this currency. example: USD transaction_amount: type: - string - 'null' readOnly: true description: '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. ' example: '0.00790' passthrough_amount: type: - string - 'null' readOnly: true description: '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. ' example: '0.00200' WhatsAppVideoSend: type: object additionalProperties: false required: - url description: 'A free-form video to send, with an optional caption. Deliverable only inside an open 24-hour customer service window; outside one, send a template instead. ' properties: url: type: string format: uri minLength: 1 description: 'Public `https` URL of the video. WhatsApp fetches it at send time, so it must still be reachable then: a signed URL has to outlive the send. We do not store or proxy the file. WhatsApp caches a fetched URL for 10 minutes and re-serves that copy for an identical URL sent again within the window; vary the URL to force a re-fetch. MP4 with H.264 video and AAC audio, up to 16 MB. ' example: https://cdn.example.com/unboxing.mp4 caption: type: string maxLength: 1024 description: Text shown beneath the video. example: How to set it up example: url: https://cdn.example.com/unboxing.mp4 caption: How to set it up ErrorBody: type: object additionalProperties: false required: - type - code - name - message - doc_url - request_id properties: type: type: string minLength: 1 description: Broad category for coarse client branching. enum: - auth_error - bad_request_error - billing_error - conflict_error - gone_error - internal_error - misdirected_error - not_found_error - not_implemented_error - payload_too_large_error - permission_error - precondition_error - rate_limit_error - service_unavailable_error - too_early_error - validation_error code: type: string minLength: 1 pattern: ^E\d{5}$ description: Opaque, stable, unique error identifier. Never reused. name: type: string minLength: 1 description: Human-readable slug for log readability. Paired with code, never replaces it. message: type: string minLength: 1 description: Human-readable description. Not stable; clients must not parse it. param: type: string minLength: 1 description: Identifies the offending field. Omitted when not applicable. doc_url: type: string minLength: 1 format: uri description: Stable link to the docs page for this error code. request_id: type: string minLength: 1 description: Request correlation ID for support and troubleshooting. Also returned in the `X-Request-Id` response header. vendor_code: type: string minLength: 1 description: 'Verbatim error code from an external system, such as an SMTP response code or a payment decline code. Present only when the code may help you resolve the error. ' details: type: array description: Per-field validation errors. Present only on validation_error responses. items: $ref: '#/components/schemas/ErrorDetail' remediation: type: string minLength: 1 description: A human-readable next step to resolve this error. Present when a recovery is known. next: type: array description: 'The steps that resolve this error. Perform them in order, re-reading after each; a `wait` or `terminal` step is always last. Present for errors with a well-defined recovery, such as unmet preconditions and conflicts. ' items: $ref: '#/components/schemas/NextAction' WhatsAppMessageTemplateComponentParameter: type: object additionalProperties: false required: - type properties: type: allOf: - $ref: '#/components/schemas/WhatsAppTemplateParameterType' description: The kind of value this parameter carries, which decides which of the fields below to send. text: type: string minLength: 1 description: The value substituted into the placeholder, as a plain string. Send it on a `text` parameter. url: type: string format: uri minLength: 1 description: 'Public `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. ' example: https://cdn.example.com/receipts/a1b2c3.png location: allOf: - $ref: '#/components/schemas/WhatsAppLocationSend' description: The point on the map a location header opens. Send it on a `location` parameter. name: type: string minLength: 1 description: 'Required 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`. ' WhatsAppContactOrg: type: object additionalProperties: false description: Where the contact works, as their card records it. properties: company: type: string example: Lucky Shrub department: type: string example: Engineering title: type: string example: Software Engineer WhatsAppInteractiveHeaderType: type: string minLength: 1 x-extensible-enum: - text - image - video - document description: '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. ' example: image WhatsAppContactAddressSend: type: object additionalProperties: false description: One postal address to put on a contact card. properties: street: type: string maxLength: 128 example: 1 Lucky Shrub Way city: type: string maxLength: 128 example: Menlo Park state: type: string maxLength: 128 example: CA zip: type: string maxLength: 128 example: '94025' country: type: string maxLength: 128 example: United States country_code: type: string maxLength: 128 description: The country as it should appear on the address, commonly the ISO two-letter code. example: US type: type: string maxLength: 64 description: 'A label for the address, shown beside it. Free text, sent exactly as written. ' example: Office WhatsAppInteractiveList: type: object additionalProperties: false required: - button_text - sections description: 'The menu the message offered: a button that opens it, and the groups of options behind it. ' properties: button_text: type: string minLength: 1 description: The label of the button that opens the menu. example: Shipping options sections: type: array description: The groups of options in the menu, in the order shown. items: $ref: '#/components/schemas/WhatsAppInteractiveListSection' WhatsAppAudio: type: object description: 'Audio content of a WhatsApp message. ' allOf: - $ref: '#/components/schemas/WhatsAppMedia' - type: object properties: voice: type: boolean description: '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. ' example: true WhatsAppInteractiveHeaderTypeWrite: type: string minLength: 1 enum: - text - image - video - document x-enum-varnames: - WhatsAppInteractiveHeaderTypeWriteText - WhatsAppInteractiveHeaderTypeWriteImage - WhatsAppInteractiveHeaderTypeWriteVideo - WhatsAppInteractiveHeaderTypeWriteDocument description: 'A header''s kind, and which field carries it. `text` is a line of copy; the rest each show a file whose address you give in `url`. A `list` accepts `text` only, and a carousel card accepts `image` or `video` only. Closed on the write side: a kind Bird cannot send to Meta is rejected rather than accepted and then failed asynchronously. ' example: image WhatsAppTemplateSend: type: object additionalProperties: false description: 'A send-by-template reference. Identify the template by its `id` or its `slug` (supply exactly one), optionally name a language, and fill its placeholders through `components`. ' oneOf: - required: - id - required: - slug properties: id: description: The template to send, by its id. $ref: '#/components/schemas/WhatsAppTemplateID' slug: allOf: - $ref: '#/components/schemas/TemplateSlug' description: The template to send, by its slug handle (for example `bird_otp`). example: bird_otp language: allOf: - $ref: '#/components/schemas/LanguageTag' description: 'Which of the template''s languages to send, as a BCP-47 tag (for example `en` or `pt-BR`); Meta''s underscore form (`pt_BR`) is accepted and normalized. Omit it to send the template''s default language, unless the template sets `language_source_required`, in which case a send naming no language is rejected. When the template does not carry the language you ask for, its own `on_missing_language` setting decides whether the closest available language is sent instead or the send is rejected. The accepted message echoes the canonical BCP-47 form of the language it resolved to, which is the language it is priced at: Meta categorizes each language separately, so a send served by a different language than the one you asked for is priced at that language''s category. ' example: pt-BR components: type: array description: 'The values that fill the template''s placeholders: one entry per content block that has placeholders, each carrying its `parameters`. A positional template takes its parameters in `{{n}}` order; a template with named parameters requires each parameter''s `name` to match one the template declares. Either way, sending parameters that do not match what the template declares returns a `422` `WhatsAppTemplateParameterMismatch`. ' items: $ref: '#/components/schemas/WhatsAppMessageTemplateComponent' examples: - id: wat_01ky4x8e4genzb7way45txfkm1 language: en components: - type: body parameters: - type: text text: '1234' - type: button parameters: - type: text text: '1234' - slug: bird_order_confirmation language: en components: - type: body parameters: - type: text name: ref text: A1B2C3D4 - type: text name: amount text: EUR 49.99 - slug: bird_otp language: en components: - type: body parameters: - type: text text: '1234' - type: button parameters: - type: text text: '1234' WhatsAppInteractiveType: type: string minLength: 1 x-extensible-enum: - button - list - cta_url - carousel - location_request_message - request_contact_info description: "Which kind of interactive message this is.\n\n- `button`: up to three tappable buttons, each sending its own identifier\n back as an inbound message. Carried in `buttons`.\n- `list`: a single button that opens a menu of rows to choose one from.\n- `cta_url`: a single button that opens a link.\n- `carousel`: 2 to 10 media cards the recipient scrolls through sideways,\n each with its own buttons. Carried in `cards`.\n- `location_request_message`: a single button that asks the recipient to\n share where they are. A tap arrives as an inbound `location` message.\n- `request_contact_info`: a single button that asks the recipient to share\n their phone number. A tap arrives as an inbound message carrying the number\n on `contact_cards`, with `origin` set to `contact_request`.\n\nThe last two name no field of their own: the ask is the whole message, and\n`body_text` is all they carry.\n\nOpen enum: WhatsApp adds interactive kinds over time, so treat an\nunrecognized value as a future kind rather than an error.\n" example: button WhatsAppLocation: type: object additionalProperties: false description: 'Location content of a WhatsApp message: a point on the map the recipient can open in their maps app. ' properties: latitude: type: number format: double description: Latitude in decimal degrees. example: 52.3702 longitude: type: number format: double description: Longitude in decimal degrees. example: 4.8952 name: type: string description: Name of the place. Absent when the sender shared a plain pin. example: Bird HQ address: type: string description: Street address of the place. Shown only when `name` is also set. example: Keizersgracht 117, Amsterdam url: type: string format: uri description: '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. ' example: https://www.google.com/maps/place/Statue+of+Liberty/@40.6246301,-74.5291919,124716m/ WhatsAppEvent: type: object additionalProperties: false required: - id - type - occurred_at properties: id: readOnly: true $ref: '#/components/schemas/WhatsAppEventID' description: ID of the event, unique within the message's timeline. type: $ref: '#/components/schemas/WhatsAppEventType' readOnly: true occurred_at: type: string format: date-time minLength: 1 readOnly: true description: When this event occurred. recipient: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppAddress' description: 'The participant this confirmation is about, on a group message. Present only on `whatsapp.delivered` and `whatsapp.read`, the two events a group send fans out: one per participant, so a group of eight produces up to eight of each. The rest describe the message as a whole and carry no recipient, because there is one hand-off to the WhatsApp network and one way for that to be refused. Absent on a one-to-one message, whose `to` already names its recipient. Never carries `group_id`: the group belongs to the message''s `to`, not to a participant. ' error: $ref: '#/components/schemas/WhatsAppError' description: Failure detail. Present only on `whatsapp.failed` and `whatsapp.rejected` events. WhatsAppReactionEventID: type: string minLength: 1 pattern: ^war_[0-9a-hjkmnp-tv-z]{26}$ example: war_01krdgeqcxet5s7t44vh8rt9mg WhatsAppEventID: type: string minLength: 1 pattern: ^ev_[0-9a-hjkmnp-tv-z]{26}$ example: ev_01krdgeqcxet5s7t44vh8rt9mg WhatsAppEventType: type: string minLength: 1 description: "Message timeline event type:\n\n- `whatsapp.accepted`: The API accepted the request.\n- `whatsapp.sent`: The message reached the WhatsApp network.\n- `whatsapp.delivered`: Delivery to the recipient's device was confirmed.\n- `whatsapp.read`: The message was read. On an outbound message the recipient\n opened it; on an inbound one Bird acknowledged it to WhatsApp for the\n business, which is what a read receipt records.\n- `whatsapp.failed`: Delivery failed permanently.\n- `whatsapp.rejected`: The message was refused before sending and not charged.\n- `whatsapp.received`: An inbound message arrived from the contact.\n\nThis is an open enum. Accept unrecognized values.\n" x-extensible-enum: - whatsapp.accepted - whatsapp.delivered - whatsapp.failed - whatsapp.read - whatsapp.received - whatsapp.rejected - whatsapp.sent example: whatsapp.delivered WhatsAppContactOrgSend: type: object additionalProperties: false description: Where the contact works, as the card should record it. properties: company: type: string maxLength: 128 example: Lucky Shrub department: type: string maxLength: 128 example: Legal title: type: string maxLength: 128 example: Lead Counsel NextAction: type: object additionalProperties: false required: - kind - description properties: kind: type: string minLength: 1 x-extensible-enum: - operation - external - wait - terminal description: "What you do about this step.\n\n- `operation`: call the operation named in `operation`, then\n read again.\n- `external`: act somewhere this API does not reach, then read\n again.\n- `wait`: nothing is asked of you, so read again later.\n- `terminal`: nothing you do resolves this, so stop retrying.\n\nTolerate a value you do not recognize: show the `description` and\noffer no action.\n" description: type: string minLength: 1 description: A short, human-readable label for the step, suitable for display. operation: type: string minLength: 1 description: 'The operationId to call. Present only when `kind` is `operation`. The operation''s own schema says how to call it; this says only which one, and what to address it with. ' params: type: object additionalProperties: type: string minLength: 1 description: 'The parameters that address the operation, by name: `{"sender_id": "…"}` for an operation on `/v1/sms/senders/{sender_id}/requirements`. A parameter the operation takes in its query string is given the same way, so an operation addressed as `?subject_id=` carries `{"subject_id": "…"}`. Every parameter the call needs is here, whether its value came from the thing you were acting on or is fixed for this step, so you can make the call from this object alone. Present only when `kind` is `operation` and the operation names a subject. A request body, when the operation takes one, is described by the operation''s own schema and never appears here. ' url: type: string format: uri description: 'A URL to open. Present only when `kind` is `external`, and only when the step has one. An external step whose `description` says to go and do something with no URL to open is normal. ' WhatsAppInteractiveReplyType: type: string minLength: 1 x-extensible-enum: - button - list description: "Which kind of tap the reply came from.\n\n- `button`: a reply button on an interactive message, or a quick-reply\n button on a template. Both carry an identifier and a label, so they read\n the same way.\n- `list`: a row chosen from a list's menu. Only this kind carries a\n `description`.\n\nOpen enum: WhatsApp adds interactive kinds over time, so treat an\nunrecognized value as a future kind rather than an error.\n" example: button WhatsAppMessageStatus: type: string minLength: 1 enum: - scheduled - accepted - sent - delivered - failed - rejected - canceled - received description: '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. ' WhatsAppImage: type: object description: 'Image content of a WhatsApp message. ' allOf: - $ref: '#/components/schemas/WhatsAppMedia' - type: object properties: caption: type: string description: Text shown beneath the image. Absent when the sender wrote none. example: Your receipt for order A1B2C3 WhatsAppFileID: type: string minLength: 1 pattern: ^waf_[0-9a-hjkmnp-tv-z]{26}$ example: waf_01krdgeqcxet5s7t44vh8rt9mg WhatsAppLocationSend: type: object additionalProperties: false required: - latitude - longitude description: '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. ' properties: latitude: type: number format: double minimum: -90 maximum: 90 description: Latitude in decimal degrees. example: 52.3702 longitude: type: number format: double minimum: -180 maximum: 180 description: Longitude in decimal degrees. example: 4.8952 name: type: string maxLength: 1000 description: Name of the place, shown above the address. example: Bird HQ address: type: string maxLength: 1000 description: Street address of the place. Shown only when `name` is also set. example: Keizersgracht 117, Amsterdam example: latitude: 52.3702 longitude: 4.8952 name: Bird HQ address: Keizersgracht 117, Amsterdam WhatsAppContactEmailSend: type: object additionalProperties: false description: One email address to put on a contact card. required: - email properties: email: type: string minLength: 1 maxLength: 254 example: barbara@example.com type: type: string maxLength: 64 description: 'A label for the address, shown beside it. Free text, sent exactly as written. ' example: Work WhatsAppMessageTemplateComponent: type: object additionalProperties: false required: - type properties: type: type: string minLength: 1 x-extensible-enum: - header - body - button - carousel description: 'Which 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. ' parameters: type: array description: 'The 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. ' items: $ref: '#/components/schemas/WhatsAppMessageTemplateComponentParameter' cards: type: array minItems: 2 maxItems: 10 description: 'The 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. ' items: $ref: '#/components/schemas/WhatsAppMessageTemplateCard' WhatsAppGroupID: type: string minLength: 1 pattern: ^wag_[0-9a-hjkmnp-tv-z]{26}$ example: wag_01krdgeqcxet5s7t44vh8rt9mg WhatsAppMessage: type: object additionalProperties: false required: - id - direction - from - to - status - created_at properties: id: readOnly: true $ref: '#/components/schemas/WhatsAppMessageID' description: 'ID of the message, assigned when the send is accepted. Pass it as `message_id` to the get-message and list-events endpoints. ' direction: type: string minLength: 1 readOnly: true enum: - outbound - inbound description: Whether the message was sent by the business (`outbound`) or received from the contact (`inbound`). from: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppAddress' description: Sender of the message. On outbound messages, the business number it was sent from; on inbound, the WhatsApp contact. to: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppAddress' description: Recipient of the message. On outbound messages, the WhatsApp contact; on inbound, the business number. template: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppMessageTemplate' description: The template the message was sent from. For authentication templates the filled-in values are not returned. text: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppText' description: Text the message carried. image: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppImage' description: Image the message carried. video: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppVideo' description: Video the message carried. audio: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppAudio' description: Audio the message carried. sticker: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppSticker' description: Sticker the message carried. document: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppDocument' description: Document the message carried. location: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppLocation' description: Location the message carried. contact_cards: readOnly: true type: array description: '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. ' items: $ref: '#/components/schemas/WhatsAppContactCard' interactive: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppInteractive' description: '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. ' in_reply_to_message_id: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppMessageID' description: '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: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppInteractiveReply' description: 'What the contact tapped, on a message answering an interactive message or a template''s quick-reply button. Inbound only. ' unsupported: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppUnsupported' description: 'Set when the contact sent content we do not model, naming the WhatsApp content type so the message is not silently empty. Inbound only. ' reactions: readOnly: true type: array description: '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. ' items: $ref: '#/components/schemas/WhatsAppReaction' status: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppMessageStatus' recipient_count: type: integer minimum: 1 readOnly: true description: '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: type: integer minimum: 0 readOnly: true description: '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: type: integer minimum: 0 readOnly: true description: '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: $ref: '#/components/schemas/WhatsAppError' description: Failure detail for a message that did not reach the recipient. Present only when the message failed or was rejected. created_at: type: string format: date-time minLength: 1 readOnly: true description: When the message was accepted for delivery. sent_at: type: - string - 'null' format: date-time readOnly: true description: When the message was handed to the WhatsApp network. Null until then. delivered_at: type: - string - 'null' format: date-time readOnly: true description: When delivery was confirmed. Null until then. read_at: type: - string - 'null' format: date-time readOnly: true description: '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: readOnly: true $ref: '#/components/schemas/MessageCost' description: 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. tags: type: array items: $ref: '#/components/schemas/Tag' description: Structured `{name, value}` filter labels applied to this message. metadata: type: object additionalProperties: true description: Arbitrary JSON metadata stored on the message. WhatsAppMessageTemplate: type: object additionalProperties: false readOnly: true description: '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). ' required: - slug - language - category - components properties: slug: allOf: - $ref: '#/components/schemas/TemplateSlug' readOnly: true description: The template's stable handle (for example `bird_otp`). example: bird_otp category: allOf: - $ref: '#/components/schemas/WhatsAppTemplateCategory' readOnly: true description: '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. ' language: allOf: - $ref: '#/components/schemas/LanguageTag' readOnly: true description: The canonical BCP-47 tag of the template variant that was sent. example: pt-BR components: type: array readOnly: true description: 'The values that filled the template''s placeholders. Empty for an authentication template, whose content is never returned. ' items: $ref: '#/components/schemas/WhatsAppMessageTemplateComponent' WhatsAppReactionUpsert: type: object additionalProperties: false required: - emoji properties: emoji: type: string minLength: 1 maxLength: 64 description: 'The emoji to place, as the character itself. Replaces your existing reaction on this message if you have one. To take a reaction back entirely, delete it rather than sending an empty value. WhatsApp takes exactly one emoji, so a value carrying more than one is refused with a `422` rather than sent. The length cap is generous because a single joined emoji is many code points: a couple-kissing one carrying two skin tones is ten, which is why the cap alone cannot express the limit. ' example: 👍 WhatsAppTemplateCategory: type: string minLength: 1 x-extensible-enum: - authentication - utility - marketing description: '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. ' WhatsAppText: type: object additionalProperties: false description: Text content of a WhatsApp message. required: - body properties: body: type: string minLength: 1 description: The message text. example: Does it come in another color? example: body: Does it come in another color? WhatsAppReaction: type: object additionalProperties: false required: - emoji - from description: 'An emoji reaction standing on a message. One entry per sender: reacting again replaces that sender''s entry rather than adding one, and removing a reaction drops it from the list. A one-to-one message therefore carries at most two, one for the contact and one for your business number. This is the folded current state, so it names no single change; the message''s reaction log is what records how each one arrived. ' properties: emoji: type: string minLength: 1 readOnly: true description: 'The emoji, as WhatsApp sent it. It is not normalized, so two emoji that render identically can differ byte for byte and compare unequal. ' example: 👍 from: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppAddress' description: '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`. ' WhatsAppInteractiveListSend: type: object additionalProperties: false required: - button_text - sections description: 'A menu of options behind a single button. The recipient taps the button, WhatsApp opens the menu, and choosing one option sends it back as a reply. ' properties: button_text: type: string minLength: 1 maxLength: 20 description: The label of the button that opens the menu. example: Shipping options sections: type: array minItems: 1 maxItems: 10 description: 'The groups of options in the menu, in the order shown. At most 10 rows across all groups combined, each carrying a label unique across the whole message. ' items: $ref: '#/components/schemas/WhatsAppInteractiveListSectionSend' example: button_text: Shipping options sections: - title: As soon as possible rows: - slug: priority_express text: Priority Mail Express description: Next day to 2 days - title: I can wait a bit rows: - slug: ground_advantage text: Ground Advantage description: 2 to 5 days MessageDirection: type: string enum: - outbound - inbound description: Whether a message was sent from the workspace (`outbound`) or received by it (`inbound`). WhatsAppInteractiveListSectionSend: type: object additionalProperties: false required: - title - rows description: 'One group of options in a list''s menu. A menu with a single group still carries a title, which WhatsApp shows above its rows. ' properties: title: type: string minLength: 1 maxLength: 24 description: The group's heading, shown above its rows. example: As soon as possible rows: type: array minItems: 1 maxItems: 10 description: 'The options in this group. A message carries at most 10 rows across all its groups combined, so this per-group maximum is not additive: more than 10 in total returns a `422` `WhatsAppInteractiveLimitExceeded`. Row labels must be unique across the whole message too, not just within a group. ' items: $ref: '#/components/schemas/WhatsAppInteractiveListRowSend' example: title: As soon as possible rows: - slug: priority_express text: Priority Mail Express description: Next day to 2 days WhatsAppContactUrl: type: object additionalProperties: false description: One website on a shared contact card. properties: url: type: string description: '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. ' example: luckyshrub.example.com type: type: string description: '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. ' example: Company WhatsAppSticker: type: object description: 'Sticker content of a WhatsApp message. ' allOf: - $ref: '#/components/schemas/WhatsAppMedia' - type: object properties: animated: type: boolean description: 'Whether the sticker is animated. Absent on an outbound message. ' example: false ErrorDetail: type: object additionalProperties: false required: - param - message properties: param: type: string minLength: 1 description: 'Dotted field path, such as `to[0].email`, `subject`, or `.`. When the request was rejected for a query parameter the endpoint does not declare, this carries that parameter''s name instead of a field path. ' message: type: string minLength: 1 description: What is wrong with this field. WhatsAppMessageSendRequest: type: object additionalProperties: false description: 'A WhatsApp message to send. Carry exactly one kind of content: a request with none returns a `422` `WhatsAppContentRequired`, and one carrying more than one returns a `422` `WhatsAppContentAmbiguous`. The schema does not express that constraint, because which combinations are available depends on the content types your workspace can send. ' required: - to properties: to: type: string minLength: 1 description: 'The message recipient: a phone number in E.164 format (for example `+31612345678`), the recipient''s business-scoped user ID (for example `US.13491208655302741918`), which addresses a WhatsApp user whose phone number you do not have, or a WhatsApp group ID (for example `wag_01krdgeqcxet5s7t44vh8rt9mg`), which sends to every participant of that group. A value that is none of these returns a `422` `WhatsAppInvalidRecipient`. One-time-passcode templates require a phone number and return a `422` `WhatsAppRecipientNotSupportedForTemplate` when sent to a business-scoped user ID. A group ID naming no group this workspace holds returns a `404` `WhatsAppGroupNotFound`, and one whose group is not active returns a `409` `WhatsAppGroupNotActive`. Content a group cannot take is refused ahead of both, so a group ID paired with interactive content returns the `422` below whether or not the group exists. ' example: '+31612345678' from: type: string minLength: 1 description: 'The business phone number to send from, in E.164 format. Omit it for a Bird-managed template, which selects its own number from its category: setting it there returns a `422` `WhatsAppSenderNotAllowed`. Every other send, whether free-form content of any kind or a template your workspace authored, requires it, and the number must be one this workspace owns. Omitting it returns a `422` `WhatsAppSenderRequired`, and naming a number this workspace cannot send from returns a `422` `WhatsAppSenderNotFound`. Naming a number this workspace owns but that sits on a different WhatsApp Business Account than an authored template returns a `422` `WhatsAppSenderWABAMismatch`. A number this workspace holds but has not finished connecting returns a `422` `WhatsAppSenderNotConnected`. Omit it for a group send too: the group sends on its own number, so naming one returns a `422` `WhatsAppSenderNotAllowed`. ' example: '+13124495648' template: allOf: - $ref: '#/components/schemas/WhatsAppTemplateSend' description: 'The template to send. A Bird-managed template selects the sender number from the template''s category, so `from` must be omitted. A template is the only content deliverable outside a customer service window. A group send takes a template your workspace authored in any category but authentication: WhatsApp does not deliver an authentication template to a group, which returns a `422` `WhatsAppGroupContentNotSupported`. A Bird-managed template sends from a Bird-owned number that no group is scoped to, so addressing one to a group returns a `422` `WhatsAppInvalidRecipient`. ' text: allOf: - $ref: '#/components/schemas/WhatsAppTextSend' description: 'Free-form text to send instead of a template. 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. A send into a closed window is refused with a `422` `WhatsAppServiceWindowClosed` before anything is created or charged; one whose window closes between accept and dispatch fails asynchronously, with `service_window_expired` on the message''s `last_error`. ' image: allOf: - $ref: '#/components/schemas/WhatsAppImageSend' description: 'A free-form image to send instead of a template. 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. A send into a closed window is refused with a `422` `WhatsAppServiceWindowClosed` before anything is created or charged; one whose window closes between accept and dispatch fails asynchronously, with `service_window_expired` on the message''s `last_error`. ' video: allOf: - $ref: '#/components/schemas/WhatsAppVideoSend' description: 'A free-form video to send instead of a template. 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. A send into a closed window is refused with a `422` `WhatsAppServiceWindowClosed` before anything is created or charged; one whose window closes between accept and dispatch fails asynchronously, with `service_window_expired` on the message''s `last_error`. ' audio: allOf: - $ref: '#/components/schemas/WhatsAppAudioSend' description: 'Free-form audio to send instead of a template. 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. A send into a closed window is refused with a `422` `WhatsAppServiceWindowClosed` before anything is created or charged; one whose window closes between accept and dispatch fails asynchronously, with `service_window_expired` on the message''s `last_error`. ' sticker: allOf: - $ref: '#/components/schemas/WhatsAppStickerSend' description: 'A free-form sticker to send instead of a template. 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. A send into a closed window is refused with a `422` `WhatsAppServiceWindowClosed` before anything is created or charged; one whose window closes between accept and dispatch fails asynchronously, with `service_window_expired` on the message''s `last_error`. ' document: allOf: - $ref: '#/components/schemas/WhatsAppDocumentSend' description: 'A free-form document to send instead of a template. 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. A send into a closed window is refused with a `422` `WhatsAppServiceWindowClosed` before anything is created or charged; one whose window closes between accept and dispatch fails asynchronously, with `service_window_expired` on the message''s `last_error`. ' location: allOf: - $ref: '#/components/schemas/WhatsAppLocationSend' description: 'A free-form location to send instead of a template. 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. A send into a closed window is refused with a `422` `WhatsAppServiceWindowClosed` before anything is created or charged; one whose window closes between accept and dispatch fails asynchronously, with `service_window_expired` on the message''s `last_error`. ' interactive: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveSend' description: 'Free-form interactive content to send instead of a template: body text plus reply buttons, a menu, a link button, media cards, or a single button asking the recipient to share their location or their phone number. 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. A send into a closed window is refused with a `422` `WhatsAppServiceWindowClosed` before anything is created or charged; one whose window closes between accept and dispatch fails asynchronously, with `service_window_expired` on the message''s `last_error`. WhatsApp does not deliver interactive content to a group, so a group recipient returns a `422` `WhatsAppGroupContentNotSupported`. ' contact_cards: type: array minItems: 1 maxItems: 5 description: 'Contact cards to send instead of a template. Up to five: WhatsApp accepts far more, and a message that opens as one name plus a count of the rest is not a card the recipient will read. ' items: $ref: '#/components/schemas/WhatsAppContactCardSend' in_reply_to_message_id: allOf: - $ref: '#/components/schemas/WhatsAppMessageID' description: 'Quote a message the contact will see above this one, the way replying in the WhatsApp client does. Name a message from the same conversation: one this workspace sent to this recipient, or received from them. Any content quotes, template or free-form. The quote is resolved before the send is accepted, so a quote WhatsApp cannot render fails this request rather than the message. An id naming no message this workspace holds, or one older than the 15 days we keep provider ids for, answers `404`; a message that never reached WhatsApp, or one from a different conversation than this send''s `to` and `from`, answers `422`. Nothing is charged either way. ' example: wam_01kya19eknftrs2s6p82asmvnh tags: type: array items: $ref: '#/components/schemas/Tag' maxItems: 20 description: 'Structured `{name, value}` labels for filtering. Tags become first-class query dimensions: filter the list endpoint by tag name. Maximum 20 tags per send. Use tags for low-cardinality dimensions (`category`, `experiment_variant`). For arbitrary structured context you do not need as a filter dimension, use `metadata` instead. ' metadata: type: object additionalProperties: true description: 'Arbitrary JSON object stored on the message and returned on API reads. Maximum 2 KB serialized. Use metadata for per-send context like internal IDs and foreign keys. For low-cardinality filterable labels, use `tags` instead. ' example: to: '+31612345678' template: slug: bird_otp language: en components: - type: body parameters: - type: text text: '1234' - type: button parameters: - type: text text: '1234' WhatsAppContactAddress: type: object additionalProperties: false description: One postal address on a shared contact card. properties: street: type: string example: 1 Hacker Way city: type: string example: Menlo Park state: type: string example: CA zip: type: string example: '94025' country: type: string example: United States country_code: type: string description: 'The country as the card holds it, left exactly as WhatsApp sent it: it describes a postal address rather than a routing destination. ' example: US type: type: string description: '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. ' example: Home Error: type: object additionalProperties: false required: - error properties: error: $ref: '#/components/schemas/ErrorBody' WhatsAppReactionEvent: type: object additionalProperties: false required: - id - emoji - status - from - occurred_at description: 'One change to a reaction on a message: a reaction placed, replaced by a different emoji, or taken back. Entries are never edited, so a sender who reacts twice and then removes it leaves three of them. ' properties: id: readOnly: true $ref: '#/components/schemas/WhatsAppReactionEventID' description: ID of this entry, unique within the message's reaction log. emoji: type: - string - 'null' readOnly: true description: 'The emoji this entry placed, as WhatsApp sent it and not normalized. Null when the entry took a reaction back rather than placing one. Always present, so null is the removal itself rather than a value we are missing. ' example: 👍 status: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppReactionEventStatus' from: readOnly: true allOf: - $ref: '#/components/schemas/WhatsAppAddress' description: 'Who made the change. Your business number on a reaction you placed, the contact on one they placed. ' error: readOnly: true $ref: '#/components/schemas/WhatsAppError' description: 'Why the change did not take effect. Always carried by a `failed` or `rejected` entry, and never by any other, so a failure always says what went wrong. Absent rather than null on the entries that did take effect. The schema leaves it optional because that is a conditional the generators do not express. `code` is drawn from the vocabulary a message send shares, less its two billing codes: a reaction is never charged, so neither `insufficient_balance` nor `price_not_found` appears here. ' occurred_at: type: string format: date-time minLength: 1 readOnly: true description: When the change was made. example: '2026-08-28T19:01:10Z' WhatsAppContactPhoneSend: type: object additionalProperties: false description: One phone number to put on a contact card. required: - phone_number properties: phone_number: type: string minLength: 1 maxLength: 32 description: 'The number to show. Send it in E.164 to get a card the recipient can message from; any other form still renders, with an invite button. ' example: '+16505551234' type: type: string maxLength: 64 description: 'A label for the number, shown beside it. Free text: WhatsApp defines no vocabulary, and the label is sent exactly as written. ' example: Mobile WhatsAppInteractiveButtonSend: type: object additionalProperties: false required: - type description: 'One button on an interactive message or on a carousel card. `type` names the kind and the field that carries it, so a button kind WhatsApp adds later arrives as another `type` here rather than as a new shape somewhere else. ' oneOf: - properties: type: const: quick_reply cta_url: not: {} required: - type - quick_reply - properties: type: const: cta_url quick_reply: not: {} required: - type - cta_url properties: type: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveButtonTypeWrite' description: Which kind of button this is, and which field carries it. quick_reply: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveQuickReplyButtonSend' description: The button's label and the handle it sends back. Send this on a `quick_reply` button. cta_url: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveCtaUrlSend' description: The button's label and the address it opens. Send this on a `cta_url` button. example: type: quick_reply quick_reply: slug: change-booking text: Change WhatsAppContactPhone: type: object additionalProperties: false description: One phone number on a shared contact card. properties: phone_number: type: string description: '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. ' example: '+16505551234' type: type: string description: '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. ' example: CELL WhatsAppContactCardSend: type: object additionalProperties: false description: 'A contact card to send. WhatsApp shows the name on the card and the rest in a profile view the recipient opens from it. A card carrying a phone number renders buttons that message or save the contact; a card without one can only be added to an address book. ' required: - name properties: name: allOf: - $ref: '#/components/schemas/WhatsAppContactNameSend' org: allOf: - $ref: '#/components/schemas/WhatsAppContactOrgSend' description: Where the contact works. birthday: type: string pattern: ^\d{4}-\d{2}-\d{2}$ description: 'The contact''s birthday, as `YYYY-MM-DD`. WhatsApp rejects any other shape, and a date no calendar holds is rejected too. ' example: '1999-01-23' phone_numbers: type: array maxItems: 10 description: 'The numbers on the card. A number in E.164 renders a button that opens a WhatsApp chat with it; one that is not renders an invite instead. ' items: $ref: '#/components/schemas/WhatsAppContactPhoneSend' emails: type: array maxItems: 10 items: $ref: '#/components/schemas/WhatsAppContactEmailSend' urls: type: array maxItems: 10 items: $ref: '#/components/schemas/WhatsAppContactUrlSend' addresses: type: array maxItems: 10 items: $ref: '#/components/schemas/WhatsAppContactAddressSend' example: name: formatted_name: Barbara J. Johnson first_name: Barbara last_name: Johnson phone_numbers: - phone_number: '+16505551234' type: Mobile WhatsAppInteractive: type: object additionalProperties: false description: '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. ' required: - type - body_text properties: type: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveType' description: Which kind of interactive message this is, and which field carries it. header: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveHeader' description: What was shown above the body. Absent when the message carried no header. body_text: type: string minLength: 1 description: The message's main text. example: Your workshop is scheduled for 9am tomorrow. footer_text: type: string description: The small print below the body. Absent when the message carried none. example: Lucky Shrub, your gateway to succulents buttons: type: array description: The buttons the message offered, in the order shown. items: $ref: '#/components/schemas/WhatsAppInteractiveButton' list: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveList' description: The menu the message offered. cta_url: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveCtaUrl' description: The link button the message offered. cards: type: array description: 'The cards the message offered, in the order they appeared, left to right. ' items: $ref: '#/components/schemas/WhatsAppInteractiveCard' WhatsAppInteractiveButton: type: object additionalProperties: false required: - type description: 'One button the message offered. The field named by `type` is the one that is present. As on the message itself, the read vocabulary is open: dispatch on `type` and treat an unrecognized value as a button kind added since. ' properties: type: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveButtonType' description: Which kind of button this is, and which field carries it. quick_reply: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveQuickReplyButton' description: The button's label and the handle it sends back. cta_url: allOf: - $ref: '#/components/schemas/WhatsAppInteractiveCtaUrl' description: The button's label and the address it opens. WhatsAppReactionEventStatus: type: string minLength: 1 enum: - received - sent - failed - rejected description: "What became of one reaction change:\n\n- `received` means the contact placed or removed the reaction and WhatsApp\n told us about it. Every inbound entry carries this.\n- `sent` means your reaction reached WhatsApp. Reactions have no delivery or\n read receipt, so this is as far as an outbound entry gets.\n- `failed` means WhatsApp refused it. `error` says why, most often because the\n contact deleted the message. The grounds we can check for ourselves (a\n message you sent, one that is itself a reaction, one over 30 days old) are\n refused when you ask, so they do not reach here.\n- `rejected` means we refused it before it reached WhatsApp, so nothing was\n sent. `error` says why.\n\nOnly `received` and `sent` entries change what stands on the message, so those\nare the ones the message's `reactions` are folded from.\n" example: received WhatsAppContactEmail: type: object additionalProperties: false description: One email address on a shared contact card. properties: email: type: string example: barbara@example.com type: type: string description: '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. ' example: Personal WhatsAppInteractiveTypeWrite: type: string minLength: 1 enum: - button - list - cta_url - carousel - location_request_message - request_contact_info x-enum-varnames: - WhatsAppInteractiveTypeWriteButton - WhatsAppInteractiveTypeWriteList - WhatsAppInteractiveTypeWriteCtaUrl - WhatsAppInteractiveTypeWriteCarousel - WhatsAppInteractiveTypeWriteLocationRequestMessage - WhatsAppInteractiveTypeWriteRequestContactInfo description: "Which kind of interactive message to send.\n\n- `button`: up to three tappable buttons, each sending its own identifier\n back as an inbound message. Carried in `buttons`.\n- `list`: a single button that opens a menu of rows to choose one from.\n- `cta_url`: a single button that opens a link, so the address stays out of\n the message body.\n- `carousel`: 2 to 10 media cards the recipient scrolls through sideways,\n each with its own buttons. Carried in `cards`.\n- `location_request_message`: a single button that asks the recipient to\n share where they are. A tap arrives as an inbound `location` message.\n- `request_contact_info`: a single button that asks the recipient to share\n their phone number. A tap arrives as an inbound message carrying the number\n on `contact_cards`, with `origin` set to `contact_request`.\n\nThe last two name no field of their own: the ask is the whole message, and\n`body_text` is all they carry.\n\nClosed on the write side: a kind Bird cannot send to Meta is rejected rather\nthan accepted and then failed asynchronously.\n" example: button WhatsAppMessageTemplateCardComponent: type: object additionalProperties: false required: - type description: The values that fill one block of one carousel card. properties: type: type: string minLength: 1 x-extensible-enum: - header - body - button description: 'Which part of the card this fills in. - `header`: the card''s image or video. - `body`: its text. - `button`: a button''s variable. ' example: header parameters: type: array description: The values that fill this part's placeholders, in placeholder order. items: $ref: '#/components/schemas/WhatsAppMessageTemplateComponentParameter' responses: Unprocessable: description: '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`. ' content: application/json: schema: $ref: '#/components/schemas/Error' NotFound: description: Resource not found content: application/json: schema: $ref: '#/components/schemas/Error' BadRequest: description: Bad request content: application/json: schema: $ref: '#/components/schemas/Error' Unauthorized: description: Authentication required content: application/json: schema: $ref: '#/components/schemas/Error' PaymentRequired: description: Insufficient balance content: application/json: schema: $ref: '#/components/schemas/Error' RateLimited: description: Rate limit exceeded headers: RateLimit: $ref: '#/components/headers/RateLimit' RateLimit-Policy: $ref: '#/components/headers/RateLimit-Policy' Retry-After: $ref: '#/components/headers/RetryAfter' content: application/json: schema: $ref: '#/components/schemas/Error' Forbidden: description: Insufficient permissions content: application/json: schema: $ref: '#/components/schemas/Error' ServiceUnavailable: description: '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. ' headers: Retry-After: $ref: '#/components/headers/RetryAfter' content: application/json: schema: $ref: '#/components/schemas/Error' Gone: description: The resource existed but is no longer available. content: application/json: schema: $ref: '#/components/schemas/Error' InternalError: description: Internal server error content: application/json: schema: $ref: '#/components/schemas/Error' Conflict: description: Resource conflict content: application/json: schema: $ref: '#/components/schemas/Error' headers: RateLimit: description: 'Remaining capacity for the request''s rate-limit policy as an IETF Structured Field. Format: `"";r=;t=`. ' schema: type: string example: '"email_send";r=842;t=35' RetryAfter: description: 'Number of seconds to wait before retrying the request. ' schema: type: integer minimum: 0 example: 35 RateLimit-Policy: description: 'Effective quota for the request''s rate-limit policy as an IETF Structured Field. Format: `"";q=;w=`. ' schema: type: string example: '"email_send";q=1000;w=60' IdempotencyReplay: description: The API includes this header when it replays the response for an earlier request that used the same `Idempotency-Key`. The API does not process the request again. schema: type: string enum: - 'true' parameters: EndingBefore: name: ending_before in: query required: false description: Cursor 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. schema: type: string PaginationLimit: name: limit in: query required: false description: Maximum number of items to return per page. schema: type: integer minimum: 1 maximum: 100 default: 25 CreatedAfter: name: created_after in: query required: false description: Limits 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. schema: type: string format: date-time example: '2026-05-01T00:00:00Z' IdempotencyKey: name: Idempotency-Key in: header required: false description: "Client-supplied key. On operations supporting request deduplication, a retained\nresponse is replayed for duplicate requests with the same key within the\nidempotency window (3 hours by default). This protection requires a workspace,\norganization, or staff-account scope. User-only and unauthenticated operations,\nstreams, and operations with a separate replay contract do not use this\nresponse replay.\n\nOn a supported operation, if idempotency protection is unavailable before execution, the API returns\n`503 IdempotencyUnavailable` (E01033) without executing this attempt. Retry with\nbackoff using the same key and request. An operation that takes effect before\nits response is retained can still execute again on retry.\n\nTwo distinct 409 errors signal misuse:\n\n- `request_in_progress` (E01004): The same key is currently being\n processed by a concurrent request. Wait briefly and retry. The lock expires within 30 seconds.\n- `idempotency_key_reuse` (E01005): The same key has already completed\n against a different request body or method. Generate a new key.\n\nRecommended key format is `/` (for example `welcome-user/usr_abc123`).\n" schema: type: string maxLength: 255 CreatedBefore: name: created_before in: query required: false description: Limits 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. schema: type: string format: date-time example: '2026-06-01T00:00:00Z' TagFilter: name: tag in: query required: false description: 'Filter 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. ' schema: type: array items: type: string StartingAfter: name: starting_after in: query required: false description: Cursor from the `next_cursor` field of a previous list response. Returns items immediately after the cursor position in the current sort order. schema: type: string securitySchemes: BearerAuth: type: http scheme: bearer description: 'Pass the API key as a bearer token in the `Authorization` header. Keys use the format `bk_{region}_*`. The prefix identifies the region and selects the API endpoint. Official Bird SDKs and the CLI derive the region from the key. ' CookieAuth: type: apiKey in: cookie name: bird_session description: '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. ' RealtimeKey: type: apiKey in: header name: X-Realtime-Key description: 'The Realtime app key. Together with `X-Realtime-Secret`, it authenticates a request to the Realtime API in addition to the workspace credential. Both values come from the app''s credentials and must belong to the calling workspace. Official Bird SDKs accept the pair as client configuration. ' RealtimeSecret: type: apiKey in: header name: X-Realtime-Secret description: 'The Realtime app secret paired with `X-Realtime-Key`. The API returns the secret only when the key is created and does not store it. Create a new key and revoke the current key if you lose the secret. Official Bird SDKs accept the pair as client configuration. '