# Destination Messages

## Send a message

**post** `/v1/messages`

Sends a message. Acceptance is durable and asynchronous: a `202` means
the message is journaled and will be delivered in order; delivery states
arrive on the chat's event sequence (and your webhooks/streams), never
on this call. The chat is created on first send to a new customer.

The `202` body's `status` is `accepted` — the message exists and is
ours to deliver, and nothing more is claimed by it. Watch
`GET /v1/streams/events` for `message.sent`, `message.delivered`,
`message.read` and `message.failed` to learn what became of it. The
stream does not replay, so connect it first; anything you miss is read
back from `GET /v1/chats/{chat}/events`, where the same message also
carries its latest `status`.

Every gate runs before anything is written, so a refusal means nothing
was stored and nothing will be sent — always safe to correct and
retry. A request that TIMES OUT is the one case that is not safe to
retry without `Idempotency-Key`. Supply one unique key per logical
message to make a timed-out acceptance safe to repeat.

An operational safety stop or the absence of an active sending number
refuses with retryable HTTP 503 (`sending_paused` or
`no_sending_number`) and a 4xxx `error.code`; nothing is journaled.
A stop on the number a conversation already uses answers the same way,
with `sending_paused` — and so does a stop on a number you named with
`from`. It is ours to lift, and you are never told to move a customer to
another number, or that your request was malformed, over one.
`from_ambiguous` joins that
class in the rare case where a `from` you supplied matches more than one
of your numbers: it names no single conversation, so nothing is chosen
for you and an operator resolves it.
If the service cannot verify the safety state, it likewise refuses with
HTTP 503 `sending_guard_unavailable` (a 3xxx `error.code`, 3009)
instead of assuming sending is allowed. These operator-dependent states
carry no `Retry-After` because there is no honest recovery time to
publish.

**Which number the message leaves by.** Omit `from` and this service
picks: a customer you have messaged before keeps the number they
already see, and a new customer is placed on the best available one.
The accepted message reports the number as `from`, and reports how it
was chosen in `from_selection`.

A conversation NEVER moves to another number. If the number a
conversation uses is no longer a registered sending number of yours, the
request is refused with HTTP 422
`pinned_line_unsendable` (a 4xxx `error.code`, 4004) and NOTHING
changes — no message is sent, no
conversation is created, and the next send to that customer resolves the
same way. `error.context.pinned_from` names the stuck number and
`error.context.available_from` names the one that could serve instead;
that number is a separate conversation with its own 24-hour window, so
you open it yourself by sending an approved template with `from` set to
it. This is deliberate: nothing is ever sent on your behalf that you did
not compose, and nothing is billed that you did not request.

Spend limits apply to every send, including templates. A projected
reservation beyond a daily or monthly cap is refused with HTTP 429
`spend_limit_exceeded` (a 4xxx `error.code`, 4006); `Retry-After` names
the exact UTC day/month reset and nothing is journaled. If the rate
catalog has no applicable rate, the service fails closed with HTTP 503
`rate_not_configured` (a 3xxx `error.code`, 3020); only operator action
can clear it, so contact support rather than retrying.
If the catalog lookup, cap, or counter is temporarily unavailable, the
retryable refusal is HTTP 503 `entitlement_unavailable` (a 3xxx
`error.code`, 3004). Crossing 80% does not change acceptance: the
response remains 202 and carries `X-Spend-Warning` once for each cap
window crossed by that request. Values are comma-separated
`brand_day`, `brand_month`, or `number_day`; the warning is emitted once
per window, not on every later send.

Every send except a reaction reserves its projected charge at accept,
including a reply inside an open customer window: the channel charges
those replies once the sending number's monthly free allowance of them is
used up, and a reply that turns out to be free has its reservation
released when its receipt arrives. A daily ceiling refuses any message
carrying a `template` part, including a template sent inside an open
window. A message without one (text, media, a reaction or another
in-window reply) is still accepted past a daily ceiling, and its charge
still counts toward the day and the month. The monthly account ceiling is
hard and stops every send at the ceiling, including a reaction.

Separately from spend, the channel limits how many DISTINCT customers a
new conversation may be started with in a moving 24-hour period. A
template send that would exceed it is refused with HTTP 429
`tier_quota_exceeded` (a 4xxx `error.code`, 4008) and nothing is
journaled; replies inside an open
customer conversation are unaffected and never count against it, and a
second template to a customer already counted in the period costs
nothing. `Retry-After` on this refusal is a MINIMUM REQUESTED WAIT
computed from the period as it stood when the request was refused: retry
no earlier than that, and a retry then may still be refused. It is not
an expiry — the earliest free place can move later as you send to
customers already counted, and can open earlier when messages fail to
deliver. **You may not be able to produce this refusal in an integration
test**, in sandbox or in live: it is raised only where the channel has
reported a limit for the account concerned, so an environment where no
limit has been reported will never return it. Handle it from the response
code rather than from having seen it.

### Header Parameters

- `"Idempotency-Key": optional string`

### Body Parameters

- `parts: array of object { body, type, preview_url }  or object { kind, type, availability, 10 more }  or object { language, name, type, 4 more }  or 10 more`

  The message content — sendable parts only, at least one.

  **One outbound part per request.** Each request has its own acceptance
  result and optional idempotency key. Each accepted request maps to one
  native channel message with its own identifier, delivery outcome and
  charge evidence. A request carrying more than one
  part is refused with HTTP 422 (`multiple_parts_not_available`, a 1xxx
  `error.code`, 1019); the API neither fans it out nor offers an atomic
  batch. Separate requests are independent, and no cross-request delivery
  or receipt order is promised.

  **A `reaction` addresses its target by this API's `Message.id`**, which
  the service resolves into the channel's own namespace before sending.
  The target must be a message this account can see in the chat the send
  addresses, and one the channel has acknowledged if this API sent it:
  otherwise the send is refused with HTTP 422 `reaction_target_not_found`
  (2049), `reaction_target_wrong_chat` (1040) or `reaction_target_not_ready`
  (2050), each pointered at the part's `message_id`. For `not_ready`, read
  the target until its `status` is `sent` — the moment its
  `channel_message_id` appears — rather than waiting on `message.sent`,
  which is journaled at acceptance. A live reaction the channel accepts
  ends at status `sent`: no delivered or read receipt is reported for one.
  It can still end `failed` if the channel refuses it on the wire. Media is
  sendable after its bytes are uploaded with
  `POST /v1/media` (the part carries that operation's `upload_ref`) or by
  naming a public https address in `source_url`, which the service fetches
  once before accepting the send and stores as an upload of its own; the
  accepted message carries the resulting `upload_ref`, never the address.
  Outbound parts never carry inbound `media_id`.

  Outside an open customer window the array must be exactly one
  `template` part; a freeform part there is refused with HTTP 422
  `outside_customer_window` (a 2xxx `error.code`, 2036) and the remedy in
  the message.

  A customer known only by a username identifier can receive every message
  type except a one-time-code template (a copy-code, one-tap or zero-tap
  autofill button), which can be delivered only to a phone number. When
  the template is known to carry such a button, the send is refused with
  HTTP 422 `recipient_phone_number_required` (a 4xxx `error.code`, 4011),
  pointered at the part, and nothing is accepted; deliver the code another
  way.

  - `Text object { body, type, preview_url }`

    A plain text message.

    - `body: string`

      The message text. URLs render as tappable links.

    - `type: "text"`

      - `"text"`

    - `preview_url: optional boolean`

      Render a preview card for the first URL in body. Defaults to false.

  - `Media object { kind, type, availability, 10 more }`

    A media message — image, video, audio, document, or sticker. The two identifiers are distinct namespaces: inbound media carries the channel's transient media_id; outbound media carries our durable upload_ref from POST /v1/media.

    - `kind: "image" or "video" or "audio" or 2 more`

      Which kind of media this is. Captions apply to image, video and document only; filename applies to document only.

      - `"image"`

      - `"video"`

      - `"audio"`

      - `"document"`

      - `"sticker"`

    - `type: "media"`

      - `"media"`

    - `availability: optional "available" or "unavailable" or "pending"`

      Response only, inbound parts only: whether the download answers the bytes. available: the file was captured and is retained. unavailable: it is not downloadable now, and unavailable_reason says why; with timed_out the download answers 409 while capture continues and can later answer 200, otherwise it answers 410, or 404 once the file is no longer known. It describes the file, not the exact HTTP answer of a later download, which can also refuse for capacity or authorization. pending: capture has not finished yet; only reads of the conversation return pending, and a message event never does. A message.received event can be recorded once every attachment has been captured or has failed, or ten minutes after the message arrived; an event part without availability may still be capturing, so retry its download as the 409 Retry-After directs. A message event keeps the value it was recorded with; reads return the current one. Historical messages received before it was recorded omit it. Never accepted on sends.

      - `"available"`

      - `"unavailable"`

      - `"pending"`

    - `byte_size: optional number`

      Response only, inbound parts only: the exact size in bytes of the retained attachment, equal to the download's Content-Length. Present only while the bytes are captured and retained — the same lifetime as a working url, ending 30 days after server receipt — so it can be absent from a message event while capture has not completed, and its absence is not final; once present it never changes. Absent rather than zero. Never accepted on sends.

    - `caption: optional string`

      Caption rendered with the media. Image, video and document only — a caption on an audio or sticker part is rejected, never dropped.

    - `filename: optional string`

      Display filename. Documents only — a filename on any other kind is rejected, never dropped.

    - `media_id: optional string`

      Inbound only: the channel's transient media identifier. It expires after seven days and is never an outbound address; an outbound value is rejected with a pointer to media_id and upload_ref as the remedy.

    - `mime_type: optional string`

      Response only, inbound parts only: the media type the channel declared when the message arrived, verbatim. It may carry parameters (for example audio/ogg; codecs=opus) and may differ from the Content-Type the download answers with, which describes the retained bytes. Present on the webhook body and on every later read with the same value. Historical messages received before it was recorded omit this field. Never accepted on sends.

    - `source_url: optional string`

      Outbound only: a public https URL of the bytes to send, as an alternative to upload_ref. Supply exactly one of upload_ref or source_url. The address is fetched once, before the send is accepted, and its exact bytes are stored as an ordinary upload in this account and test/live partition; the accepted message and every later read carry that upload's upload_ref, never the address, and no retry fetches it again. The URL must be absolute https on the default port with no credentials and no fragment, must resolve to a public address (every redirect hop included, at most three), and must answer with the complete bytes and their exact media type within the fetch deadline. The bytes are then held to the same media type and size rules as POST /v1/media for the part's kind; a mismatch is refused, never converted.

    - `unavailable_reason: optional string`

      Response only, present only when availability is unavailable. An open string: the values today are timed_out, capture_failed and expired, and new values may be added. timed_out: the file was still being retrieved ten minutes after the message arrived, so its message event was recorded without it; retrieval continues and the file may become downloadable later, which reads of the conversation report. capture_failed: the file could not be retrieved. expired: the file was retrieved and is no longer retained, or is no longer known (the download then answers 410 or 404). Treat an unknown value as capture_failed. Never accepted on sends.

    - `upload_ref: optional string`

      Outbound only: our durable handle returned by POST /v1/media. Handles visibly begin upload. and are reusable within the account and test/live partition that created them.

    - `url: optional string`

      Response only: stable attachment download URL requiring the account Bearer key on every request. Pending capture returns 409 with Retry-After; retained bytes expire 30 days after server receipt. Historical unbound messages omit this field. Never accepted on sends.

    - `voice: optional boolean`

      Audio only. On a send it is optional: true sends the audio as a voice note; false, or leaving it out, sends it as an ordinary audio file. A voice note needs an upload stored as audio/ogg that holds mono Opus audio, sent by upload_ref or source_url within the existing audio size limit. The bytes are sent as supplied: they are never converted or inspected, so encode them as mono Opus before uploading. The recipient sees a play button only when the voice file is 512 KB or smaller; a larger voice note is still delivered but must be downloaded before it plays. Supported on sends to individual chats only. On incoming audio parts it is response-only: whether the channel reported the audio as a voice note, verbatim. true means it was recorded with the messaging client's voice-recording feature; false means it was sent as an audio file. Present on the webhook body and on every later read with the same value. Absent on an incoming part means the channel did not say, never that the audio is not a voice note. Other kinds never carry it, and historical messages received before it was recorded omit it.

  - `Template object { language, name, type, 4 more }`

    An approved message template — the one part type sendable outside an open customer service window. name and language identify the approved template; parameters fills its named placeholders. Authentication templates use this same generic part: their published parameter schema names exactly one parameter, `code` — the one-time code you generate, 1–15 characters — and the message shows it in its body and behind its code button. Every sendable template is registered and approved ahead of the send and publishes its own parameter schema — a send referencing an unknown template, a missing or unknown parameter, or a value that breaks the template's rules is rejected with a 422 naming the exact field.

    - `language: string`

      The template's language-and-locale code, e.g. en_US.

    - `name: string`

      The approved template's name. Lowercase letters, digits and underscores only.

    - `type: "template"`

      - `"template"`

    - `buttons: optional array of object { index, payload, type }`

      Send-time values for this approved template's own dynamic buttons, in ascending index order: a url button whose destination ends in {{1}} takes the value appended there and is REQUIRED; a quick_reply button takes the payload returned to you when it is tapped and is OPTIONAL — omit it and the button returns its label. Fixed buttons have no slot. The approved template's send_schema lists the (index, type) slots and an x-rule stating which are required; omit buttons entirely for a template with no url slot when the labels are what you want back.

      - `index: number`

      - `payload: string`

      - `type: "quick_reply" or "url"`

        - `"quick_reply"`

        - `"url"`

    - `cards: optional array of object { card_index, kind, upload_ref, 2 more }`

      Per-card values for an approved media-card carousel. The approved template's send_schema provides structural bounds and an x-rule listing the exact card order, media kind, named body values, and dynamic button slots enforced by runtime L2 validation. Omit for non-carousel templates.

      - `card_index: number`

      - `kind: "image" or "video"`

        - `"image"`

        - `"video"`

      - `upload_ref: string`

        A durable handle returned by POST /v1/media. It is resolved within this key's account and partition, then consumed into a transient channel media identifier only at delivery.

      - `buttons: optional array of object { index, payload, type }`

        - `index: number`

        - `payload: string`

        - `type: "quick_reply" or "url"`

          - `"quick_reply"`

          - `"url"`

      - `parameters: optional unknown`

        Named values for this card's body placeholders.

    - `header: optional object { kind, upload_ref }`

      The uploaded media used for this approved template's media header. Omit unless this template's send_schema requires it.

      - `kind: "image" or "video" or "document"`

        - `"image"`

        - `"video"`

        - `"document"`

      - `upload_ref: string`

        A durable handle returned by POST /v1/media.

    - `parameters: optional unknown`

      Named values for the template's placeholders. The shape is per-template: each registered template publishes its own parameter schema — hand THAT schema to your model when composing a specific template. Omit for templates with no placeholders.

  - `InteractiveList object { body, button, sections, 3 more }`

    An interactive list message: body text plus a button that opens a sectioned list of selectable rows. The customer's selection arrives as an inbound interactive_reply part with kind list_reply, carrying the chosen row's id.

    - `body: string`

      The message body.

    - `button: string`

      Label of the button that opens the list.

    - `sections: array of object { rows, title }`

      The list's sections, each holding selectable rows. At most 10 rows in total across ALL sections combined — not 10 per section.

      - `rows: array of object { id, title, description }`

        This section's rows. The 10-row limit is on the TOTAL across all sections, so a second section reduces what this one may hold.

        - `id: string`

          Your identifier for the row — echoed back as the interactive_reply's id.

        - `title: string`

          The row's visible title.

        - `description: optional string`

          Secondary text under the title.

      - `title: optional string`

        Section title. Required when the list has more than one section.

    - `type: "interactive_list"`

      - `"interactive_list"`

    - `footer: optional string`

      Footer text below the body.

    - `header: optional string`

      Header text above the body.

  - `InteractiveButtons object { body, buttons, type, 3 more }`

    An interactive reply-buttons message: body text plus up to three tappable buttons. The customer's tap arrives as an inbound interactive_reply part with kind button_reply, carrying the tapped button's id.

    - `body: string`

      The message body.

    - `buttons: array of object { id, title }`

      The tappable buttons — at most three.

      - `id: string`

        Your identifier for the button — echoed back as the interactive_reply's id.

      - `title: string`

        The button's visible label.

    - `type: "interactive_buttons"`

      - `"interactive_buttons"`

    - `footer: optional string`

      Footer text below the body.

    - `header: optional string`

      Header text above the body.

    - `media_header: optional object { kind, upload_ref }`

      Uploaded image, video, or document above the body. Use either header or media_header, never both.

      - `kind: "image" or "video" or "document"`

        - `"image"`

        - `"video"`

        - `"document"`

      - `upload_ref: string`

        A durable handle returned by POST /v1/media.

  - `InteractiveCtaURL object { body, display_text, type, 4 more }`

    An interactive call-to-action message: body text plus one button that opens a URL.

    - `body: string`

      The message body.

    - `display_text: string`

      The button's visible label.

    - `type: "interactive_cta_url"`

      - `"interactive_cta_url"`

    - `url: string`

      The URL the button opens. Must be absolute, with an http or https scheme.

    - `footer: optional string`

      Footer text below the body.

    - `header: optional string`

      Header text above the body.

    - `media_header: optional object { kind, upload_ref, filename }`

      Uploaded image, video, or document above the body. Use either header or media_header, never both.

      - `kind: "image" or "video" or "document"`

        - `"image"`

        - `"video"`

        - `"document"`

      - `upload_ref: string`

        A durable handle returned by POST /v1/media.

      - `filename: optional string`

        The file name the customer sees on a document header. Documents only.

  - `InteractiveCarousel object { body, cards, type }`

    An interactive carousel: one message whose body sits above 2 to 10 horizontally scrolling cards. Each card shows an uploaded image, or a video from a public link, and either one button that opens a URL or reply buttons. A tap on a card's reply button arrives as an inbound template_reply part whose payload is that button's id; nothing else in the reply identifies the card, so the id you choose is the card's identity.

    - `body: string`

      The message text shown above the cards.

    - `cards: array of object { card_index, header, body, 2 more }`

      The cards, left to right.

      - `card_index: number`

        This card's zero-based position: the first card is 0, the next 1, and so on, in array order.

      - `header: object { kind, upload_ref, url }`

        The card's media: an image named by upload_ref, or a video named by the public https url it plays from. Every card carries the same kind.

        - `kind: "image" or "video"`

          - `"image"`

          - `"video"`

        - `upload_ref: optional string`

          A durable handle returned by POST /v1/media for an image upload. Image cards only.

        - `url: optional string`

          The public https address the card's video plays from. Video cards only. It is sent exactly as written and never fetched by this API, so it must be publicly reachable and serve a playable video; a link that does not play is accepted and then reported as a failed delivery (message.failed with channel_refused).

      - `body: optional string`

        Optional text below the card's image or video, with at most two line breaks.

      - `reply_buttons: optional array of object { id, title }`

        Buttons the customer taps to reply. Use either url_button or reply_buttons on every card.

        - `id: string`

          Your identifier for the button, echoed back as the template_reply's payload. Unique across the whole carousel.

        - `title: string`

          The button's visible label.

      - `url_button: optional object { display_text, url }`

        One button that opens a URL. Use either url_button or reply_buttons on every card.

        - `display_text: string`

          The button's visible label.

        - `url: string`

          The absolute https URL the button opens.

    - `type: "interactive_carousel"`

      - `"interactive_carousel"`

  - `LocationRequest object { body, type }`

    Asks the customer to share a location. The response arrives as a location part.

    - `body: string`

      Prompt shown above the send-location button.

    - `type: "location_request"`

      - `"location_request"`

  - `ContactRequest object { body, type }`

    Asks the customer to share their own contact card with one tap. Useful when you know the customer only by a username identifier and need a phone number. The answer arrives as an ordinary contacts part whose card carries origin contact_request; a card the customer shares by hand carries origin other instead. The answer is not linked to your request as a reply: its message carries no reply_to_message_id, so match it by origin and timing. Sendable inside the customer window, to individual chats only.

    - `body: string`

      Prompt shown above the share-contact button.

    - `type: "contact_request"`

      - `"contact_request"`

  - `AddressRequest object { body, country, type }`

    Reserved for asking an eligible customer in India to submit a structured address. Sending this part is not available until business and customer eligibility can be verified at acceptance. Address responses already arrive as address_reply parts.

    - `body: string`

      Prompt shown above the address form.

    - `country: "IN"`

      ISO country code. Structured address requests are currently available only in India.

      - `"IN"`

    - `type: "address_request"`

      - `"address_request"`

  - `Contacts object { contacts, type }`

    One or more contact cards.

    - `contacts: array of object { name, addresses, birthday, 5 more }`

      The contact cards to send. The channel permits far more; this API caps a message at five, because a message carrying hundreds of cards is a mistake rather than a use case.

      - `name: object { formatted_name, first_name, last_name, 3 more }`

        - `formatted_name: string`

          The contact's full display name.

        - `first_name: optional string`

        - `last_name: optional string`

        - `middle_name: optional string`

        - `prefix: optional string`

        - `suffix: optional string`

      - `addresses: optional array of object { city, country, country_code, 4 more }`

        - `city: optional string`

        - `country: optional string`

        - `country_code: optional string`

        - `kind: optional string`

          A label for the address, e.g. WORK, HOME.

        - `state: optional string`

        - `street: optional string`

        - `zip: optional string`

      - `birthday: optional string`

        The contact's birthday as YYYY-MM-DD.

      - `emails: optional array of object { email, kind }`

        - `email: string`

        - `kind: optional string`

          A label for the address, e.g. WORK, HOME.

      - `org: optional object { company, department, title }`

        - `company: optional string`

        - `department: optional string`

        - `title: optional string`

      - `origin: optional string`

        Response-only: how the customer came to share this card, as the channel reported it, verbatim. contact_request means they tapped the share button on a contact_request you sent; other means they shared a card themselves. New values may appear, so treat any other value as unrecognised rather than as either of these. Absent when the channel did not say, and on cards received before it was recorded. A send carrying it is rejected with a pointer at the member.

      - `phones: optional array of object { phone, kind, messaging_id }`

        - `phone: string`

          The phone number, ideally in +E.164 form.

        - `kind: optional string`

          A label for the number, e.g. CELL, MAIN, WORK, HOME.

        - `messaging_id: optional string`

          The contact's messaging-account identifier on this channel, when known — makes the card openable in the messaging app.

      - `urls: optional array of object { url, kind }`

        - `url: string`

        - `kind: optional string`

          A label for the URL, e.g. WORK, HOME.

    - `type: "contacts"`

      - `"contacts"`

  - `Location object { latitude, longitude, type, 2 more }`

    A location pin.

    - `latitude: number`

      Latitude in decimal degrees.

    - `longitude: number`

      Longitude in decimal degrees.

    - `type: "location"`

      - `"location"`

    - `address: optional string`

      The place's address, shown under the name.

    - `name: optional string`

      The place's name, shown on the pin.

  - `Reaction object { emoji, type, channel_message_id, message_id }`

    An emoji reaction to an earlier message in the chat. message_id is this API's Message.id for the target in both directions: it is what you send, and it is appended to an inbound reaction whenever the target is a message of the same chat. channel_message_id is the channel's opaque identifier and appears on inbound reactions only.

    - `emoji: string`

      A single emoji. Send an empty string to remove a previous reaction to the same message.

    - `type: "reaction"`

      - `"reaction"`

    - `channel_message_id: optional string`

      Inbound only: the channel's opaque identifier for the message being reacted to. Always present on an inbound reaction, beside message_id whenever the target resolved. Match it literally to Message.channel_message_id in the chat transcript; never parse it or compare it with Message.id.

    - `message_id: optional string`

      This API's Message.id for the message being reacted to. Outbound: required, and the API resolves it into the channel namespace before sending, so the message must be one this account can see in the chat the send addresses, and — for a message this API sent — one the channel has already acknowledged. Inbound: present whenever the target is a message of the same chat, so you can keep only this identifier; absent when the chat does not hold the target.

- `to: string`

  The customer to message: the exact opaque customer address this API
  reports as the chat's `customer` field (read with `GET /v1/chats` or
  `GET /v1/chats/{chat}`), copied verbatim and never parsed, built or
  reformatted. It is
  usually the customer's phone number in the channel's digits-only form
  (country code first, no `+` and no punctuation, such as `15550101234`);
  a customer who uses a username and has not shared a phone number is
  identified by a username identifier instead (such as
  `US.13491208655302741918`). Treat both the same way: copy the value.

  **The value is matched literally and never normalized**, because the
  address is the channel's to define, not ours to rewrite. It is the
  conversation's identity, so two spellings of the same number are two
  different conversations, each with its own window and its own event
  sequence.

  Concretely: the value is compared verbatim to the chat's `customer`
  field, the customer id exactly as that customer's inbound messages
  arrived. Events identify the chat by `chat_id`, not by customer, so read
  the chat's `customer` with `GET /v1/chats/{chat}`. A leading `+`
  makes it a different value — `+12059367930` does not find the
  conversation delivered as `12059367930` — and such a send is refused
  with `outside_customer_window` (`2036`) as if that customer had never
  written. When this account does hold an open window under the value
  without the `+`, the refusal's `details[0].message` says so and names
  both values, so the fix is to resend with the delivered value or on
  `POST /v1/chats/{chat}/messages`, which needs no address at all.

- `exclude: optional array of string`

  Numbers NOT to send from, in E.164 — a filter on a choice this service
  is making for you.

  It applies only when `from` is omitted; supplying both is refused. It
  affects this request alone: nothing is stored, and no operator setting
  changes.

  It cannot move a conversation. If it names the number an existing
  conversation already uses, or removes every number that could have
  served, the send is refused with `exclude_leaves_no_sender` rather than
  quietly sent from a number you asked to avoid.

- `from: optional string`

  The number to send FROM, in E.164 (e.g. `+15550001111`). Together with
  `to` it names the exact conversation: one customer, one of your numbers.

  **Omit it and this service chooses**, exactly as it always has — a
  conversation you have already had continues on its own number, and a new
  customer is placed on the best available one. Omitting the field and
  sending an empty string are DIFFERENT requests: an empty string is
  refused rather than treated as "choose for me", because a client that
  computed nothing did not mean to hand the choice back.

  Supply it to reply on a specific number — in particular the second
  conversation a customer starts by messaging another of your numbers,
  which is its own chat with its own window.

  The number must be one of your registered sending numbers:
  `from_not_available` (422, a 1xxx `error.code`, 1013) if it is not one
  of your numbers at all, and `from_not_sendable` (422, a 4xxx
  `error.code`, 4003) if it is yours but is no longer registered to
  send. A number that is only PAUSED is neither of those — it answers
  retryable HTTP 503 `sending_paused` (a 4xxx `error.code`, 4005), the
  same as every other paused send, because that one is ours to lift and
  worth retrying shortly. It is
  matched on its digits, so `+15550001111` and `1 (555) 000-1111` are the
  same number — unlike `to`, which is the channel's own address form and
  is matched literally.

  Naming a number never moves an existing conversation onto it. It does
  decide where a BRAND-NEW one lives: if you have never exchanged a
  message with this customer, the number you name becomes the one they
  keep, and a later omitted-`from` send to them continues on it. That is
  the same rule as ever — a customer's number is fixed by the first
  contact, whoever chose it — and it is why naming a number for a customer
  you have already messaged changes nothing about their existing
  conversation.

- `reply_to_message_id: optional string`

  Our opaque `Message.id` for an earlier message in this same chat. When
  present, this send is displayed as a visual reply to that message.

  Unknown ids return `reply_target_not_found`; an id from another chat
  returns `reply_target_wrong_chat`; and an outbound message whose channel
  acknowledgement has not arrived returns `reply_target_not_ready`.
  Nothing is accepted when any of those refusals is returned.

  The accepted message reads back with the same `reply_to_message_id`,
  and an inbound message that quotes one of yours carries the quoted
  message's `id` in the same field.

- `send_at: optional string`

  RESERVED — scheduled sends are not yet available. The field is declared
  so its later arrival is purely additive; until then ANY value here,
  including a past timestamp, is rejected loudly with HTTP 422
  `send_at_not_available` (a 1xxx `error.code`, 1039). Omit the field
  entirely.

### Returns

- `AcceptedMessage object { id, chat_id, created_at, 9 more }`

  An accepted send: the message, plus how its sending number was chosen.

  - `id: string`

    This API's message id. Put this value in an outbound reaction's `message_id`; an inbound reaction to this message carries the same value in its `message_id`, so this is the one identifier you need to keep. Mark-as-read targets the latest unread inbound message in a chat, so it takes the chat id.

  - `chat_id: string`

    The chat this message belongs to.

  - `created_at: string`

  - `direction: "inbound" or "outbound"`

    `inbound` (from the customer) or `outbound` (sent by you).

    - `"inbound"`

    - `"outbound"`

  - `parts: array of object { body, type, preview_url }  or object { kind, type, availability, 10 more }  or object { language, name, type, 4 more }  or 17 more`

    The message content — sendable parts, plus the inbound-only types on
    inbound messages. Empty only when `zero_retention` is true, or when an
    idempotent retry returns an ephemeral message whose content retention
    cleanup has removed.

    - `Text object { body, type, preview_url }`

      A plain text message.

      - `body: string`

        The message text. URLs render as tappable links.

      - `type: "text"`

        - `"text"`

      - `preview_url: optional boolean`

        Render a preview card for the first URL in body. Defaults to false.

    - `Media object { kind, type, availability, 10 more }`

      A media message — image, video, audio, document, or sticker. The two identifiers are distinct namespaces: inbound media carries the channel's transient media_id; outbound media carries our durable upload_ref from POST /v1/media.

      - `kind: "image" or "video" or "audio" or 2 more`

        Which kind of media this is. Captions apply to image, video and document only; filename applies to document only.

        - `"image"`

        - `"video"`

        - `"audio"`

        - `"document"`

        - `"sticker"`

      - `type: "media"`

        - `"media"`

      - `availability: optional "available" or "unavailable" or "pending"`

        Response only, inbound parts only: whether the download answers the bytes. available: the file was captured and is retained. unavailable: it is not downloadable now, and unavailable_reason says why; with timed_out the download answers 409 while capture continues and can later answer 200, otherwise it answers 410, or 404 once the file is no longer known. It describes the file, not the exact HTTP answer of a later download, which can also refuse for capacity or authorization. pending: capture has not finished yet; only reads of the conversation return pending, and a message event never does. A message.received event can be recorded once every attachment has been captured or has failed, or ten minutes after the message arrived; an event part without availability may still be capturing, so retry its download as the 409 Retry-After directs. A message event keeps the value it was recorded with; reads return the current one. Historical messages received before it was recorded omit it. Never accepted on sends.

        - `"available"`

        - `"unavailable"`

        - `"pending"`

      - `byte_size: optional number`

        Response only, inbound parts only: the exact size in bytes of the retained attachment, equal to the download's Content-Length. Present only while the bytes are captured and retained — the same lifetime as a working url, ending 30 days after server receipt — so it can be absent from a message event while capture has not completed, and its absence is not final; once present it never changes. Absent rather than zero. Never accepted on sends.

      - `caption: optional string`

        Caption rendered with the media. Image, video and document only — a caption on an audio or sticker part is rejected, never dropped.

      - `filename: optional string`

        Display filename. Documents only — a filename on any other kind is rejected, never dropped.

      - `media_id: optional string`

        Inbound only: the channel's transient media identifier. It expires after seven days and is never an outbound address; an outbound value is rejected with a pointer to media_id and upload_ref as the remedy.

      - `mime_type: optional string`

        Response only, inbound parts only: the media type the channel declared when the message arrived, verbatim. It may carry parameters (for example audio/ogg; codecs=opus) and may differ from the Content-Type the download answers with, which describes the retained bytes. Present on the webhook body and on every later read with the same value. Historical messages received before it was recorded omit this field. Never accepted on sends.

      - `source_url: optional string`

        Outbound only: a public https URL of the bytes to send, as an alternative to upload_ref. Supply exactly one of upload_ref or source_url. The address is fetched once, before the send is accepted, and its exact bytes are stored as an ordinary upload in this account and test/live partition; the accepted message and every later read carry that upload's upload_ref, never the address, and no retry fetches it again. The URL must be absolute https on the default port with no credentials and no fragment, must resolve to a public address (every redirect hop included, at most three), and must answer with the complete bytes and their exact media type within the fetch deadline. The bytes are then held to the same media type and size rules as POST /v1/media for the part's kind; a mismatch is refused, never converted.

      - `unavailable_reason: optional string`

        Response only, present only when availability is unavailable. An open string: the values today are timed_out, capture_failed and expired, and new values may be added. timed_out: the file was still being retrieved ten minutes after the message arrived, so its message event was recorded without it; retrieval continues and the file may become downloadable later, which reads of the conversation report. capture_failed: the file could not be retrieved. expired: the file was retrieved and is no longer retained, or is no longer known (the download then answers 410 or 404). Treat an unknown value as capture_failed. Never accepted on sends.

      - `upload_ref: optional string`

        Outbound only: our durable handle returned by POST /v1/media. Handles visibly begin upload. and are reusable within the account and test/live partition that created them.

      - `url: optional string`

        Response only: stable attachment download URL requiring the account Bearer key on every request. Pending capture returns 409 with Retry-After; retained bytes expire 30 days after server receipt. Historical unbound messages omit this field. Never accepted on sends.

      - `voice: optional boolean`

        Audio only. On a send it is optional: true sends the audio as a voice note; false, or leaving it out, sends it as an ordinary audio file. A voice note needs an upload stored as audio/ogg that holds mono Opus audio, sent by upload_ref or source_url within the existing audio size limit. The bytes are sent as supplied: they are never converted or inspected, so encode them as mono Opus before uploading. The recipient sees a play button only when the voice file is 512 KB or smaller; a larger voice note is still delivered but must be downloaded before it plays. Supported on sends to individual chats only. On incoming audio parts it is response-only: whether the channel reported the audio as a voice note, verbatim. true means it was recorded with the messaging client's voice-recording feature; false means it was sent as an audio file. Present on the webhook body and on every later read with the same value. Absent on an incoming part means the channel did not say, never that the audio is not a voice note. Other kinds never carry it, and historical messages received before it was recorded omit it.

    - `Template object { language, name, type, 4 more }`

      An approved message template — the one part type sendable outside an open customer service window. name and language identify the approved template; parameters fills its named placeholders. Authentication templates use this same generic part: their published parameter schema names exactly one parameter, `code` — the one-time code you generate, 1–15 characters — and the message shows it in its body and behind its code button. Every sendable template is registered and approved ahead of the send and publishes its own parameter schema — a send referencing an unknown template, a missing or unknown parameter, or a value that breaks the template's rules is rejected with a 422 naming the exact field.

      - `language: string`

        The template's language-and-locale code, e.g. en_US.

      - `name: string`

        The approved template's name. Lowercase letters, digits and underscores only.

      - `type: "template"`

        - `"template"`

      - `buttons: optional array of object { index, payload, type }`

        Send-time values for this approved template's own dynamic buttons, in ascending index order: a url button whose destination ends in {{1}} takes the value appended there and is REQUIRED; a quick_reply button takes the payload returned to you when it is tapped and is OPTIONAL — omit it and the button returns its label. Fixed buttons have no slot. The approved template's send_schema lists the (index, type) slots and an x-rule stating which are required; omit buttons entirely for a template with no url slot when the labels are what you want back.

        - `index: number`

        - `payload: string`

        - `type: "quick_reply" or "url"`

          - `"quick_reply"`

          - `"url"`

      - `cards: optional array of object { card_index, kind, upload_ref, 2 more }`

        Per-card values for an approved media-card carousel. The approved template's send_schema provides structural bounds and an x-rule listing the exact card order, media kind, named body values, and dynamic button slots enforced by runtime L2 validation. Omit for non-carousel templates.

        - `card_index: number`

        - `kind: "image" or "video"`

          - `"image"`

          - `"video"`

        - `upload_ref: string`

          A durable handle returned by POST /v1/media. It is resolved within this key's account and partition, then consumed into a transient channel media identifier only at delivery.

        - `buttons: optional array of object { index, payload, type }`

          - `index: number`

          - `payload: string`

          - `type: "quick_reply" or "url"`

            - `"quick_reply"`

            - `"url"`

        - `parameters: optional unknown`

          Named values for this card's body placeholders.

      - `header: optional object { kind, upload_ref }`

        The uploaded media used for this approved template's media header. Omit unless this template's send_schema requires it.

        - `kind: "image" or "video" or "document"`

          - `"image"`

          - `"video"`

          - `"document"`

        - `upload_ref: string`

          A durable handle returned by POST /v1/media.

      - `parameters: optional unknown`

        Named values for the template's placeholders. The shape is per-template: each registered template publishes its own parameter schema — hand THAT schema to your model when composing a specific template. Omit for templates with no placeholders.

    - `InteractiveList object { body, button, sections, 3 more }`

      An interactive list message: body text plus a button that opens a sectioned list of selectable rows. The customer's selection arrives as an inbound interactive_reply part with kind list_reply, carrying the chosen row's id.

      - `body: string`

        The message body.

      - `button: string`

        Label of the button that opens the list.

      - `sections: array of object { rows, title }`

        The list's sections, each holding selectable rows. At most 10 rows in total across ALL sections combined — not 10 per section.

        - `rows: array of object { id, title, description }`

          This section's rows. The 10-row limit is on the TOTAL across all sections, so a second section reduces what this one may hold.

          - `id: string`

            Your identifier for the row — echoed back as the interactive_reply's id.

          - `title: string`

            The row's visible title.

          - `description: optional string`

            Secondary text under the title.

        - `title: optional string`

          Section title. Required when the list has more than one section.

      - `type: "interactive_list"`

        - `"interactive_list"`

      - `footer: optional string`

        Footer text below the body.

      - `header: optional string`

        Header text above the body.

    - `InteractiveButtons object { body, buttons, type, 3 more }`

      An interactive reply-buttons message: body text plus up to three tappable buttons. The customer's tap arrives as an inbound interactive_reply part with kind button_reply, carrying the tapped button's id.

      - `body: string`

        The message body.

      - `buttons: array of object { id, title }`

        The tappable buttons — at most three.

        - `id: string`

          Your identifier for the button — echoed back as the interactive_reply's id.

        - `title: string`

          The button's visible label.

      - `type: "interactive_buttons"`

        - `"interactive_buttons"`

      - `footer: optional string`

        Footer text below the body.

      - `header: optional string`

        Header text above the body.

      - `media_header: optional object { kind, upload_ref }`

        Uploaded image, video, or document above the body. Use either header or media_header, never both.

        - `kind: "image" or "video" or "document"`

          - `"image"`

          - `"video"`

          - `"document"`

        - `upload_ref: string`

          A durable handle returned by POST /v1/media.

    - `InteractiveCtaURL object { body, display_text, type, 4 more }`

      An interactive call-to-action message: body text plus one button that opens a URL.

      - `body: string`

        The message body.

      - `display_text: string`

        The button's visible label.

      - `type: "interactive_cta_url"`

        - `"interactive_cta_url"`

      - `url: string`

        The URL the button opens. Must be absolute, with an http or https scheme.

      - `footer: optional string`

        Footer text below the body.

      - `header: optional string`

        Header text above the body.

      - `media_header: optional object { kind, upload_ref, filename }`

        Uploaded image, video, or document above the body. Use either header or media_header, never both.

        - `kind: "image" or "video" or "document"`

          - `"image"`

          - `"video"`

          - `"document"`

        - `upload_ref: string`

          A durable handle returned by POST /v1/media.

        - `filename: optional string`

          The file name the customer sees on a document header. Documents only.

    - `InteractiveCarousel object { body, cards, type }`

      An interactive carousel: one message whose body sits above 2 to 10 horizontally scrolling cards. Each card shows an uploaded image, or a video from a public link, and either one button that opens a URL or reply buttons. A tap on a card's reply button arrives as an inbound template_reply part whose payload is that button's id; nothing else in the reply identifies the card, so the id you choose is the card's identity.

      - `body: string`

        The message text shown above the cards.

      - `cards: array of object { card_index, header, body, 2 more }`

        The cards, left to right.

        - `card_index: number`

          This card's zero-based position: the first card is 0, the next 1, and so on, in array order.

        - `header: object { kind, upload_ref, url }`

          The card's media: an image named by upload_ref, or a video named by the public https url it plays from. Every card carries the same kind.

          - `kind: "image" or "video"`

            - `"image"`

            - `"video"`

          - `upload_ref: optional string`

            A durable handle returned by POST /v1/media for an image upload. Image cards only.

          - `url: optional string`

            The public https address the card's video plays from. Video cards only. It is sent exactly as written and never fetched by this API, so it must be publicly reachable and serve a playable video; a link that does not play is accepted and then reported as a failed delivery (message.failed with channel_refused).

        - `body: optional string`

          Optional text below the card's image or video, with at most two line breaks.

        - `reply_buttons: optional array of object { id, title }`

          Buttons the customer taps to reply. Use either url_button or reply_buttons on every card.

          - `id: string`

            Your identifier for the button, echoed back as the template_reply's payload. Unique across the whole carousel.

          - `title: string`

            The button's visible label.

        - `url_button: optional object { display_text, url }`

          One button that opens a URL. Use either url_button or reply_buttons on every card.

          - `display_text: string`

            The button's visible label.

          - `url: string`

            The absolute https URL the button opens.

      - `type: "interactive_carousel"`

        - `"interactive_carousel"`

    - `LocationRequest object { body, type }`

      Asks the customer to share a location. The response arrives as a location part.

      - `body: string`

        Prompt shown above the send-location button.

      - `type: "location_request"`

        - `"location_request"`

    - `ContactRequest object { body, type }`

      Asks the customer to share their own contact card with one tap. Useful when you know the customer only by a username identifier and need a phone number. The answer arrives as an ordinary contacts part whose card carries origin contact_request; a card the customer shares by hand carries origin other instead. The answer is not linked to your request as a reply: its message carries no reply_to_message_id, so match it by origin and timing. Sendable inside the customer window, to individual chats only.

      - `body: string`

        Prompt shown above the share-contact button.

      - `type: "contact_request"`

        - `"contact_request"`

    - `AddressRequest object { body, country, type }`

      Reserved for asking an eligible customer in India to submit a structured address. Sending this part is not available until business and customer eligibility can be verified at acceptance. Address responses already arrive as address_reply parts.

      - `body: string`

        Prompt shown above the address form.

      - `country: "IN"`

        ISO country code. Structured address requests are currently available only in India.

        - `"IN"`

      - `type: "address_request"`

        - `"address_request"`

    - `Contacts object { contacts, type }`

      One or more contact cards.

      - `contacts: array of object { name, addresses, birthday, 5 more }`

        The contact cards to send. The channel permits far more; this API caps a message at five, because a message carrying hundreds of cards is a mistake rather than a use case.

        - `name: object { formatted_name, first_name, last_name, 3 more }`

          - `formatted_name: string`

            The contact's full display name.

          - `first_name: optional string`

          - `last_name: optional string`

          - `middle_name: optional string`

          - `prefix: optional string`

          - `suffix: optional string`

        - `addresses: optional array of object { city, country, country_code, 4 more }`

          - `city: optional string`

          - `country: optional string`

          - `country_code: optional string`

          - `kind: optional string`

            A label for the address, e.g. WORK, HOME.

          - `state: optional string`

          - `street: optional string`

          - `zip: optional string`

        - `birthday: optional string`

          The contact's birthday as YYYY-MM-DD.

        - `emails: optional array of object { email, kind }`

          - `email: string`

          - `kind: optional string`

            A label for the address, e.g. WORK, HOME.

        - `org: optional object { company, department, title }`

          - `company: optional string`

          - `department: optional string`

          - `title: optional string`

        - `origin: optional string`

          Response-only: how the customer came to share this card, as the channel reported it, verbatim. contact_request means they tapped the share button on a contact_request you sent; other means they shared a card themselves. New values may appear, so treat any other value as unrecognised rather than as either of these. Absent when the channel did not say, and on cards received before it was recorded. A send carrying it is rejected with a pointer at the member.

        - `phones: optional array of object { phone, kind, messaging_id }`

          - `phone: string`

            The phone number, ideally in +E.164 form.

          - `kind: optional string`

            A label for the number, e.g. CELL, MAIN, WORK, HOME.

          - `messaging_id: optional string`

            The contact's messaging-account identifier on this channel, when known — makes the card openable in the messaging app.

        - `urls: optional array of object { url, kind }`

          - `url: string`

          - `kind: optional string`

            A label for the URL, e.g. WORK, HOME.

      - `type: "contacts"`

        - `"contacts"`

    - `Location object { latitude, longitude, type, 2 more }`

      A location pin.

      - `latitude: number`

        Latitude in decimal degrees.

      - `longitude: number`

        Longitude in decimal degrees.

      - `type: "location"`

        - `"location"`

      - `address: optional string`

        The place's address, shown under the name.

      - `name: optional string`

        The place's name, shown on the pin.

    - `Reaction object { emoji, type, channel_message_id, message_id }`

      An emoji reaction to an earlier message in the chat. message_id is this API's Message.id for the target in both directions: it is what you send, and it is appended to an inbound reaction whenever the target is a message of the same chat. channel_message_id is the channel's opaque identifier and appears on inbound reactions only.

      - `emoji: string`

        A single emoji. Send an empty string to remove a previous reaction to the same message.

      - `type: "reaction"`

        - `"reaction"`

      - `channel_message_id: optional string`

        Inbound only: the channel's opaque identifier for the message being reacted to. Always present on an inbound reaction, beside message_id whenever the target resolved. Match it literally to Message.channel_message_id in the chat transcript; never parse it or compare it with Message.id.

      - `message_id: optional string`

        This API's Message.id for the message being reacted to. Outbound: required, and the API resolves it into the channel namespace before sending, so the message must be one this account can see in the chat the send addresses, and — for a message this API sent — one the channel has already acknowledged. Inbound: present whenever the target is a message of the same chat, so you can keep only this identifier; absent when the chat does not hold the target.

    - `InteractiveReply object { id, kind, title, 2 more }`

      Inbound only — never sendable. The customer's selection from an interactive_list (kind list_reply) or interactive_buttons (kind button_reply) message, carrying the id you assigned to the chosen row or button.

      - `id: string`

        The id you assigned to the chosen row or button, exactly as you sent it.

      - `kind: "list_reply" or "button_reply"`

        Which interactive message kind was answered.

        - `"list_reply"`

        - `"button_reply"`

      - `title: string`

        The chosen row's or button's visible title.

      - `type: "interactive_reply"`

        - `"interactive_reply"`

      - `description: optional string`

        The chosen list row's secondary text, when it had one.

    - `TemplateReply object { payload, text, type }`

      Inbound only — never sendable. The customer tapped a quick-reply button on an approved template you sent, or a reply button on a card of an interactive_carousel you sent. payload is the value you set on that button: the template's quick_reply slot payload (or the button's label when you left the slot out), or the carousel card button's id. text is the label the customer saw. A carousel tap carries nothing else that identifies the card, so the id you chose is the card's only identity. For a template tap, the message's reply_to_message_id names the template message that carried the button. Distinct from interactive_reply, which answers an interactive_buttons or interactive_list message.

      - `payload: string`

        The quick_reply slot's payload from your send, or the button label when the slot was omitted; for a carousel card, the tapped reply button's id.

      - `text: string`

        The tapped button's label, as the customer saw it.

      - `type: "template_reply"`

        - `"template_reply"`

    - `AddressReply object { type, values, saved_address_id }`

      Inbound only: the structured values submitted in response to an address request.

      - `type: "address_reply"`

        - `"address_reply"`

      - `values: object { address, building_name, city, 8 more }`

        - `address: optional string`

        - `building_name: optional string`

        - `city: optional string`

        - `floor_number: optional string`

        - `house_number: optional string`

        - `landmark_area: optional string`

        - `name: optional string`

        - `phone_number: optional string`

        - `pin_code: optional string`

        - `state: optional string`

        - `tower_number: optional string`

      - `saved_address_id: optional string`

        Identifier of the selected saved address, when one was selected.

    - `Order object { catalog_id, items, type, text }`

      Inbound only — never sendable. An order the customer placed from a product catalog.

      - `catalog_id: string`

        The catalog the ordered items belong to.

      - `items: array of object { currency, price, product_retailer_id, quantity }`

        The ordered items.

        - `currency: string`

          ISO 4217 currency code for price.

        - `price: number`

          The per-item price at order time.

        - `product_retailer_id: string`

          Your identifier for the product, as registered in the catalog.

        - `quantity: number`

      - `type: "order"`

        - `"order"`

      - `text: optional string`

        Free text the customer attached to the order, when any.

    - `Referral object { type, body, click_id, 7 more }`

      Inbound only — never sendable. The ad or post context a customer's first message arrived from (for example an ad whose call to action opens a chat). A referral also opens a free-entry-point customer window — see the chat's customer_window.

      - `type: "referral"`

        - `"referral"`

      - `body: optional string`

        The ad's body text at click time.

      - `click_id: optional string`

        The click identifier assigned by the ad platform, for attribution.

      - `headline: optional string`

        The ad's headline at click time.

      - `image_url: optional string`

        URL of the ad's image creative, when media_kind is image.

      - `media_kind: optional string`

        The ad creative's media kind, e.g. image or video.

      - `source_id: optional string`

        The ad or post id.

      - `source_kind: optional string`

        What the source was, e.g. ad or post.

      - `source_url: optional string`

        The URL of the ad or post the customer came from.

      - `video_url: optional string`

        URL of the ad's video creative, when media_kind is video.

    - `System object { body, type, event }`

      Inbound only — never sendable. A system event in the chat, such as the customer changing their number. New event kinds appear over time; body is always present and human-readable.

      - `body: string`

        Human-readable description of the system event.

      - `type: "system"`

        - `"system"`

      - `event: optional string`

        The system event's kind, when the channel identifies one. New kinds appear over time — treat unknown values as informational.

    - `Unsupported object { raw, type, kind }`

      Inbound only — never sendable. A message kind this API does not yet type natively, carried as a typed passthrough: raw holds the channel payload, so a new message kind is never a black box or a silent drop. Native part types for popular kinds are added over time; this part is the compatibility guarantee in the meantime.

      - `raw: unknown`

        The channel payload — any JSON value: object, array, string, number, boolean, or null. Deliberately unconstrained, because the whole point of this part is to carry a shape this API does not yet know. For rows written by API version 1.3.0 or later, object member order, number formatting and duplicated members survive storage and read-back. The canonical encoder may compact insignificant whitespace before storage. Rows written before API version 1.3.0 retain normalized JSON only: member order and number formatting may differ, and only the last duplicated member survives.

      - `type: "unsupported"`

        - `"unsupported"`

      - `kind: optional string`

        The channel's name for the message kind, when it declares one.

  - `status: string`

    The message's delivery state as last reported.

    Inbound messages read `received`: they arrived, and no delivery of ours
    ran. A message you sent starts `accepted` and moves through `sent`,
    `delivered` and `read` as the channel reports them, with `failed` as the
    definitive negative outcome.

    `unknown` means exactly that: the send is INDETERMINATE and we decline
    to guess. It is NOT terminal — an indeterminate send is never re-sent,
    and it resolves to `sent`, `delivered`, `read` or `failed` when the
    channel's own report arrives — so keep observing rather than treating it
    as an outcome.

    States are reported asynchronously on the chat's event sequence, and the
    set grows additively: treat a value you do not recognise as "no
    information" rather than failing on it.

    Retention of this published status follows the chat transcript policy:
    there is no scheduled pruning, but no fixed minimum availability period
    is published yet. The present absence of a scheduled age-based sweep is
    not a promise of indefinite availability.

    UNDER A `sk_test_` KEY THESE ARE SIMULATED. A test-key send is reported
    `sent` and then `delivered` within milliseconds, every time; `delivered`
    there means a simulator accepted it, not that a device received it. Live
    delivery can lag by hours, can never report `delivered` at all, and can
    fail after the channel accepted the message — so do not calibrate
    timeouts or "delivered means it arrived" logic against a test key.

  - `channel_message_id: optional string`

    The channel's opaque identifier for this message, when the channel has assigned one. An inbound reaction's `channel_message_id` matches this field literally; never parse it or compare it with `id`. It is absent while an accepted outbound message has not yet received a channel identifier, and until it is present an inbound reaction to this message carries only `channel_message_id`.

  - `failure: optional object { code, message }`

    Why a failed message failed, when we have a reason worth publishing.

    Present only on `status` `failed`, and not on every one of those: a
    message can fail for a reason outside the published vocabulary and carry
    no `failure` at all. Its absence means "no published reason", never "no
    reason". Most messages the channel refused do carry one: a specific code
    where one is published, and `channel_refused` for most of the rest; a few
    kinds of refusal still carry none.

    `status` stays the thing to branch on. This is additive detail beside it,
    so an integration written before this field existed still sees a terminal
    `failed` and behaves exactly as it did.

    - `code: string`

      The specific reason.

      `sender_deregistered` — the number this message was going out from is no
      longer a registered sending number, and the message never left. This is
      terminal: it is not a pause and it does not clear, so retrying the same
      message cannot succeed and that conversation is over. Reach this customer
      by starting a new one on another of your numbers with a template, exactly
      as you would any customer whose 24-hour window has closed; `GET /v1/chats`
      shows which number each conversation uses. You are not billed for a
      message that failed this way.

      `quality_hold_expired` — this marketing message remained held by an
      automatic safety control on its sending line for ten minutes and was not
      sent. This is terminal: retrying the same message cannot succeed on that
      conversation. Send marketing from another of your numbers with a template,
      or contact support about the sending line. You are not billed for a message
      that failed this way.

      `recipient_country_restricted` — the business behind the sending number
      is not currently permitted to message customers in this recipient's
      country, so the message was not delivered. This is terminal for the
      message: sending it again cannot succeed while the restriction stands.
      Most often this is because the business has not yet completed business
      verification: complete it, and until then message customers in the
      sending number's own country. If the business is already verified, review
      the messaging policy for the business's category. It can arrive either as
      an immediate `failed` or after the message was first reported `sent`;
      both mean the same thing. You are not billed for a message that failed
      this way.

      `payment_method_required` — the business behind the sending number has
      no working payment method on its messaging account, so this message was
      not sent. This is terminal for the message: retrying it cannot succeed
      until a payment method is added. Add or update the payment method in the
      messaging account's billing settings, then send again as a new message.
      Replies inside an open 24-hour customer window are charged too once the
      sending number's monthly free allowance of them is used up, so they also
      need a working payment method and can fail this way. It can arrive either
      as an immediate `failed` or after the message was first reported `sent`;
      both mean the same thing.
      You are not billed for a message that failed this way.

      `recipient_phone_number_required` — this customer is known only by a
      username identifier, not a phone number, and this message can be
      delivered only to a phone number, so it was not sent. This is terminal
      for the message: sending it again to this customer cannot succeed.
      One-time-code templates (copy code, one-tap or zero-tap autofill) are the
      usual cause; deliver the code another way. Other messages, including
      templates without a one-time-code button, can still be sent to this
      customer. When the template is known in advance to carry a one-time-code
      button, the send is refused at once with the error
      `recipient_phone_number_required` instead and nothing is accepted. It can
      arrive either as an immediate `failed` or after the message was first
      reported `sent`; both mean the same thing. You are not billed for a
      message that failed this way.

      `business_restricted` — this message was refused because the business
      behind the sending number is most likely restricted on its messaging
      account, so it was not sent. This is terminal for the message: sending
      it again will not succeed while the restriction stands. Review the status
      of the business's messaging account, and if the business believes it
      complies with the messaging policies, request a review of the
      restriction. It can arrive either as an immediate `failed` or after the
      message was first reported `sent`; both mean the same thing. You are not
      billed for a message that failed this way.

      `channel_refused` — this message was refused upstream and was not sent,
      for a reason that has no more specific code here, and it will not be
      retried automatically. Contact support with this message's `id` to learn
      the reason and whether sending it again can succeed. It can arrive either as an immediate `failed` or after the message
      was first reported `sent`; both mean the same thing. You are not billed
      for a message that failed this way. A more specific code may be published
      for some of these refusals in a later release; treat a code you do not
      recognise exactly as you treat this one.

    - `message: string`

      Human-readable, written for a person reading it. Do not branch on it.

  - `from: optional string`

    The number this message left by, or arrived on, in E.164 (leading `+`).
    Every message in one conversation carries the same value — a
    conversation never moves to another number. Absent only if the number is
    no longer one of yours.

  - `from_selection: optional object { reason }`

    Present only when this service chose the sending number for you — that
    is, when you omitted `from`. A send that named `from` omits this rather
    than reporting a choice nobody made.

    - `reason: string`

      Why this number was used. Exactly one of:

      - `reused_active_chat` — this customer already had a conversation and it
        continues on the same number.
      - `new_best_number` — there was no existing conversation, so the best
        available number was chosen.

      Like every other vocabulary here it is a plain string that may grow
      additively; treat a value you do not recognise as "chosen for you".

  - `reply_to_message_id: optional string`

    This API's `id` of the earlier message in this chat that this message
    replies to. On an inbound message it is the message the customer quoted;
    on an outbound message it is the `reply_to_message_id` you sent. Look the
    value up in this chat's transcript to show what was answered; it is the
    same namespace as `id`, never a channel identifier.

    Absent when there is no reply context this API can resolve — a message
    that quotes nothing, or one that quotes a message this API never
    recorded in this chat. Treat absence as "no reply context available",
    not as proof the message was not a reply.

  - `zero_retention: optional true`

    True when the message is under zero-day retention: its content is not
    kept, so later reads and idempotent retries return empty `parts`. The
    original send response still carries the content.
    Retention applies only within Linq's systems. The WhatsApp channel
    operator's own processing and retention of message content are governed
    by its terms and are not affected by this setting.

    - `true`

### Example

```http
curl https://whatsapp.messages.api.linqapp.com/v1/messages \
    -H 'Content-Type: application/json' \
    -H "Authorization: Bearer $LINQ_WHATSAPP_API_KEY" \
    -d '{
          "parts": [
            {
              "body": "body",
              "type": "text"
            }
          ],
          "to": "to"
        }'
```

#### Response

```json
{
  "id": "id",
  "chat_id": "chat_id",
  "created_at": "2019-12-27T18:11:19.117Z",
  "direction": "inbound",
  "parts": [
    {
      "body": "body",
      "type": "text",
      "preview_url": true
    }
  ],
  "status": "status",
  "channel_message_id": "channel_message_id",
  "failure": {
    "code": "code",
    "message": "message"
  },
  "from": "from",
  "from_selection": {
    "reason": "reason"
  },
  "reply_to_message_id": "reply_to_message_id",
  "zero_retention": true
}
```

## Domain Types

### Accepted Message

- `AcceptedMessage object { id, chat_id, created_at, 9 more }`

  An accepted send: the message, plus how its sending number was chosen.

  - `id: string`

    This API's message id. Put this value in an outbound reaction's `message_id`; an inbound reaction to this message carries the same value in its `message_id`, so this is the one identifier you need to keep. Mark-as-read targets the latest unread inbound message in a chat, so it takes the chat id.

  - `chat_id: string`

    The chat this message belongs to.

  - `created_at: string`

  - `direction: "inbound" or "outbound"`

    `inbound` (from the customer) or `outbound` (sent by you).

    - `"inbound"`

    - `"outbound"`

  - `parts: array of object { body, type, preview_url }  or object { kind, type, availability, 10 more }  or object { language, name, type, 4 more }  or 17 more`

    The message content — sendable parts, plus the inbound-only types on
    inbound messages. Empty only when `zero_retention` is true, or when an
    idempotent retry returns an ephemeral message whose content retention
    cleanup has removed.

    - `Text object { body, type, preview_url }`

      A plain text message.

      - `body: string`

        The message text. URLs render as tappable links.

      - `type: "text"`

        - `"text"`

      - `preview_url: optional boolean`

        Render a preview card for the first URL in body. Defaults to false.

    - `Media object { kind, type, availability, 10 more }`

      A media message — image, video, audio, document, or sticker. The two identifiers are distinct namespaces: inbound media carries the channel's transient media_id; outbound media carries our durable upload_ref from POST /v1/media.

      - `kind: "image" or "video" or "audio" or 2 more`

        Which kind of media this is. Captions apply to image, video and document only; filename applies to document only.

        - `"image"`

        - `"video"`

        - `"audio"`

        - `"document"`

        - `"sticker"`

      - `type: "media"`

        - `"media"`

      - `availability: optional "available" or "unavailable" or "pending"`

        Response only, inbound parts only: whether the download answers the bytes. available: the file was captured and is retained. unavailable: it is not downloadable now, and unavailable_reason says why; with timed_out the download answers 409 while capture continues and can later answer 200, otherwise it answers 410, or 404 once the file is no longer known. It describes the file, not the exact HTTP answer of a later download, which can also refuse for capacity or authorization. pending: capture has not finished yet; only reads of the conversation return pending, and a message event never does. A message.received event can be recorded once every attachment has been captured or has failed, or ten minutes after the message arrived; an event part without availability may still be capturing, so retry its download as the 409 Retry-After directs. A message event keeps the value it was recorded with; reads return the current one. Historical messages received before it was recorded omit it. Never accepted on sends.

        - `"available"`

        - `"unavailable"`

        - `"pending"`

      - `byte_size: optional number`

        Response only, inbound parts only: the exact size in bytes of the retained attachment, equal to the download's Content-Length. Present only while the bytes are captured and retained — the same lifetime as a working url, ending 30 days after server receipt — so it can be absent from a message event while capture has not completed, and its absence is not final; once present it never changes. Absent rather than zero. Never accepted on sends.

      - `caption: optional string`

        Caption rendered with the media. Image, video and document only — a caption on an audio or sticker part is rejected, never dropped.

      - `filename: optional string`

        Display filename. Documents only — a filename on any other kind is rejected, never dropped.

      - `media_id: optional string`

        Inbound only: the channel's transient media identifier. It expires after seven days and is never an outbound address; an outbound value is rejected with a pointer to media_id and upload_ref as the remedy.

      - `mime_type: optional string`

        Response only, inbound parts only: the media type the channel declared when the message arrived, verbatim. It may carry parameters (for example audio/ogg; codecs=opus) and may differ from the Content-Type the download answers with, which describes the retained bytes. Present on the webhook body and on every later read with the same value. Historical messages received before it was recorded omit this field. Never accepted on sends.

      - `source_url: optional string`

        Outbound only: a public https URL of the bytes to send, as an alternative to upload_ref. Supply exactly one of upload_ref or source_url. The address is fetched once, before the send is accepted, and its exact bytes are stored as an ordinary upload in this account and test/live partition; the accepted message and every later read carry that upload's upload_ref, never the address, and no retry fetches it again. The URL must be absolute https on the default port with no credentials and no fragment, must resolve to a public address (every redirect hop included, at most three), and must answer with the complete bytes and their exact media type within the fetch deadline. The bytes are then held to the same media type and size rules as POST /v1/media for the part's kind; a mismatch is refused, never converted.

      - `unavailable_reason: optional string`

        Response only, present only when availability is unavailable. An open string: the values today are timed_out, capture_failed and expired, and new values may be added. timed_out: the file was still being retrieved ten minutes after the message arrived, so its message event was recorded without it; retrieval continues and the file may become downloadable later, which reads of the conversation report. capture_failed: the file could not be retrieved. expired: the file was retrieved and is no longer retained, or is no longer known (the download then answers 410 or 404). Treat an unknown value as capture_failed. Never accepted on sends.

      - `upload_ref: optional string`

        Outbound only: our durable handle returned by POST /v1/media. Handles visibly begin upload. and are reusable within the account and test/live partition that created them.

      - `url: optional string`

        Response only: stable attachment download URL requiring the account Bearer key on every request. Pending capture returns 409 with Retry-After; retained bytes expire 30 days after server receipt. Historical unbound messages omit this field. Never accepted on sends.

      - `voice: optional boolean`

        Audio only. On a send it is optional: true sends the audio as a voice note; false, or leaving it out, sends it as an ordinary audio file. A voice note needs an upload stored as audio/ogg that holds mono Opus audio, sent by upload_ref or source_url within the existing audio size limit. The bytes are sent as supplied: they are never converted or inspected, so encode them as mono Opus before uploading. The recipient sees a play button only when the voice file is 512 KB or smaller; a larger voice note is still delivered but must be downloaded before it plays. Supported on sends to individual chats only. On incoming audio parts it is response-only: whether the channel reported the audio as a voice note, verbatim. true means it was recorded with the messaging client's voice-recording feature; false means it was sent as an audio file. Present on the webhook body and on every later read with the same value. Absent on an incoming part means the channel did not say, never that the audio is not a voice note. Other kinds never carry it, and historical messages received before it was recorded omit it.

    - `Template object { language, name, type, 4 more }`

      An approved message template — the one part type sendable outside an open customer service window. name and language identify the approved template; parameters fills its named placeholders. Authentication templates use this same generic part: their published parameter schema names exactly one parameter, `code` — the one-time code you generate, 1–15 characters — and the message shows it in its body and behind its code button. Every sendable template is registered and approved ahead of the send and publishes its own parameter schema — a send referencing an unknown template, a missing or unknown parameter, or a value that breaks the template's rules is rejected with a 422 naming the exact field.

      - `language: string`

        The template's language-and-locale code, e.g. en_US.

      - `name: string`

        The approved template's name. Lowercase letters, digits and underscores only.

      - `type: "template"`

        - `"template"`

      - `buttons: optional array of object { index, payload, type }`

        Send-time values for this approved template's own dynamic buttons, in ascending index order: a url button whose destination ends in {{1}} takes the value appended there and is REQUIRED; a quick_reply button takes the payload returned to you when it is tapped and is OPTIONAL — omit it and the button returns its label. Fixed buttons have no slot. The approved template's send_schema lists the (index, type) slots and an x-rule stating which are required; omit buttons entirely for a template with no url slot when the labels are what you want back.

        - `index: number`

        - `payload: string`

        - `type: "quick_reply" or "url"`

          - `"quick_reply"`

          - `"url"`

      - `cards: optional array of object { card_index, kind, upload_ref, 2 more }`

        Per-card values for an approved media-card carousel. The approved template's send_schema provides structural bounds and an x-rule listing the exact card order, media kind, named body values, and dynamic button slots enforced by runtime L2 validation. Omit for non-carousel templates.

        - `card_index: number`

        - `kind: "image" or "video"`

          - `"image"`

          - `"video"`

        - `upload_ref: string`

          A durable handle returned by POST /v1/media. It is resolved within this key's account and partition, then consumed into a transient channel media identifier only at delivery.

        - `buttons: optional array of object { index, payload, type }`

          - `index: number`

          - `payload: string`

          - `type: "quick_reply" or "url"`

            - `"quick_reply"`

            - `"url"`

        - `parameters: optional unknown`

          Named values for this card's body placeholders.

      - `header: optional object { kind, upload_ref }`

        The uploaded media used for this approved template's media header. Omit unless this template's send_schema requires it.

        - `kind: "image" or "video" or "document"`

          - `"image"`

          - `"video"`

          - `"document"`

        - `upload_ref: string`

          A durable handle returned by POST /v1/media.

      - `parameters: optional unknown`

        Named values for the template's placeholders. The shape is per-template: each registered template publishes its own parameter schema — hand THAT schema to your model when composing a specific template. Omit for templates with no placeholders.

    - `InteractiveList object { body, button, sections, 3 more }`

      An interactive list message: body text plus a button that opens a sectioned list of selectable rows. The customer's selection arrives as an inbound interactive_reply part with kind list_reply, carrying the chosen row's id.

      - `body: string`

        The message body.

      - `button: string`

        Label of the button that opens the list.

      - `sections: array of object { rows, title }`

        The list's sections, each holding selectable rows. At most 10 rows in total across ALL sections combined — not 10 per section.

        - `rows: array of object { id, title, description }`

          This section's rows. The 10-row limit is on the TOTAL across all sections, so a second section reduces what this one may hold.

          - `id: string`

            Your identifier for the row — echoed back as the interactive_reply's id.

          - `title: string`

            The row's visible title.

          - `description: optional string`

            Secondary text under the title.

        - `title: optional string`

          Section title. Required when the list has more than one section.

      - `type: "interactive_list"`

        - `"interactive_list"`

      - `footer: optional string`

        Footer text below the body.

      - `header: optional string`

        Header text above the body.

    - `InteractiveButtons object { body, buttons, type, 3 more }`

      An interactive reply-buttons message: body text plus up to three tappable buttons. The customer's tap arrives as an inbound interactive_reply part with kind button_reply, carrying the tapped button's id.

      - `body: string`

        The message body.

      - `buttons: array of object { id, title }`

        The tappable buttons — at most three.

        - `id: string`

          Your identifier for the button — echoed back as the interactive_reply's id.

        - `title: string`

          The button's visible label.

      - `type: "interactive_buttons"`

        - `"interactive_buttons"`

      - `footer: optional string`

        Footer text below the body.

      - `header: optional string`

        Header text above the body.

      - `media_header: optional object { kind, upload_ref }`

        Uploaded image, video, or document above the body. Use either header or media_header, never both.

        - `kind: "image" or "video" or "document"`

          - `"image"`

          - `"video"`

          - `"document"`

        - `upload_ref: string`

          A durable handle returned by POST /v1/media.

    - `InteractiveCtaURL object { body, display_text, type, 4 more }`

      An interactive call-to-action message: body text plus one button that opens a URL.

      - `body: string`

        The message body.

      - `display_text: string`

        The button's visible label.

      - `type: "interactive_cta_url"`

        - `"interactive_cta_url"`

      - `url: string`

        The URL the button opens. Must be absolute, with an http or https scheme.

      - `footer: optional string`

        Footer text below the body.

      - `header: optional string`

        Header text above the body.

      - `media_header: optional object { kind, upload_ref, filename }`

        Uploaded image, video, or document above the body. Use either header or media_header, never both.

        - `kind: "image" or "video" or "document"`

          - `"image"`

          - `"video"`

          - `"document"`

        - `upload_ref: string`

          A durable handle returned by POST /v1/media.

        - `filename: optional string`

          The file name the customer sees on a document header. Documents only.

    - `InteractiveCarousel object { body, cards, type }`

      An interactive carousel: one message whose body sits above 2 to 10 horizontally scrolling cards. Each card shows an uploaded image, or a video from a public link, and either one button that opens a URL or reply buttons. A tap on a card's reply button arrives as an inbound template_reply part whose payload is that button's id; nothing else in the reply identifies the card, so the id you choose is the card's identity.

      - `body: string`

        The message text shown above the cards.

      - `cards: array of object { card_index, header, body, 2 more }`

        The cards, left to right.

        - `card_index: number`

          This card's zero-based position: the first card is 0, the next 1, and so on, in array order.

        - `header: object { kind, upload_ref, url }`

          The card's media: an image named by upload_ref, or a video named by the public https url it plays from. Every card carries the same kind.

          - `kind: "image" or "video"`

            - `"image"`

            - `"video"`

          - `upload_ref: optional string`

            A durable handle returned by POST /v1/media for an image upload. Image cards only.

          - `url: optional string`

            The public https address the card's video plays from. Video cards only. It is sent exactly as written and never fetched by this API, so it must be publicly reachable and serve a playable video; a link that does not play is accepted and then reported as a failed delivery (message.failed with channel_refused).

        - `body: optional string`

          Optional text below the card's image or video, with at most two line breaks.

        - `reply_buttons: optional array of object { id, title }`

          Buttons the customer taps to reply. Use either url_button or reply_buttons on every card.

          - `id: string`

            Your identifier for the button, echoed back as the template_reply's payload. Unique across the whole carousel.

          - `title: string`

            The button's visible label.

        - `url_button: optional object { display_text, url }`

          One button that opens a URL. Use either url_button or reply_buttons on every card.

          - `display_text: string`

            The button's visible label.

          - `url: string`

            The absolute https URL the button opens.

      - `type: "interactive_carousel"`

        - `"interactive_carousel"`

    - `LocationRequest object { body, type }`

      Asks the customer to share a location. The response arrives as a location part.

      - `body: string`

        Prompt shown above the send-location button.

      - `type: "location_request"`

        - `"location_request"`

    - `ContactRequest object { body, type }`

      Asks the customer to share their own contact card with one tap. Useful when you know the customer only by a username identifier and need a phone number. The answer arrives as an ordinary contacts part whose card carries origin contact_request; a card the customer shares by hand carries origin other instead. The answer is not linked to your request as a reply: its message carries no reply_to_message_id, so match it by origin and timing. Sendable inside the customer window, to individual chats only.

      - `body: string`

        Prompt shown above the share-contact button.

      - `type: "contact_request"`

        - `"contact_request"`

    - `AddressRequest object { body, country, type }`

      Reserved for asking an eligible customer in India to submit a structured address. Sending this part is not available until business and customer eligibility can be verified at acceptance. Address responses already arrive as address_reply parts.

      - `body: string`

        Prompt shown above the address form.

      - `country: "IN"`

        ISO country code. Structured address requests are currently available only in India.

        - `"IN"`

      - `type: "address_request"`

        - `"address_request"`

    - `Contacts object { contacts, type }`

      One or more contact cards.

      - `contacts: array of object { name, addresses, birthday, 5 more }`

        The contact cards to send. The channel permits far more; this API caps a message at five, because a message carrying hundreds of cards is a mistake rather than a use case.

        - `name: object { formatted_name, first_name, last_name, 3 more }`

          - `formatted_name: string`

            The contact's full display name.

          - `first_name: optional string`

          - `last_name: optional string`

          - `middle_name: optional string`

          - `prefix: optional string`

          - `suffix: optional string`

        - `addresses: optional array of object { city, country, country_code, 4 more }`

          - `city: optional string`

          - `country: optional string`

          - `country_code: optional string`

          - `kind: optional string`

            A label for the address, e.g. WORK, HOME.

          - `state: optional string`

          - `street: optional string`

          - `zip: optional string`

        - `birthday: optional string`

          The contact's birthday as YYYY-MM-DD.

        - `emails: optional array of object { email, kind }`

          - `email: string`

          - `kind: optional string`

            A label for the address, e.g. WORK, HOME.

        - `org: optional object { company, department, title }`

          - `company: optional string`

          - `department: optional string`

          - `title: optional string`

        - `origin: optional string`

          Response-only: how the customer came to share this card, as the channel reported it, verbatim. contact_request means they tapped the share button on a contact_request you sent; other means they shared a card themselves. New values may appear, so treat any other value as unrecognised rather than as either of these. Absent when the channel did not say, and on cards received before it was recorded. A send carrying it is rejected with a pointer at the member.

        - `phones: optional array of object { phone, kind, messaging_id }`

          - `phone: string`

            The phone number, ideally in +E.164 form.

          - `kind: optional string`

            A label for the number, e.g. CELL, MAIN, WORK, HOME.

          - `messaging_id: optional string`

            The contact's messaging-account identifier on this channel, when known — makes the card openable in the messaging app.

        - `urls: optional array of object { url, kind }`

          - `url: string`

          - `kind: optional string`

            A label for the URL, e.g. WORK, HOME.

      - `type: "contacts"`

        - `"contacts"`

    - `Location object { latitude, longitude, type, 2 more }`

      A location pin.

      - `latitude: number`

        Latitude in decimal degrees.

      - `longitude: number`

        Longitude in decimal degrees.

      - `type: "location"`

        - `"location"`

      - `address: optional string`

        The place's address, shown under the name.

      - `name: optional string`

        The place's name, shown on the pin.

    - `Reaction object { emoji, type, channel_message_id, message_id }`

      An emoji reaction to an earlier message in the chat. message_id is this API's Message.id for the target in both directions: it is what you send, and it is appended to an inbound reaction whenever the target is a message of the same chat. channel_message_id is the channel's opaque identifier and appears on inbound reactions only.

      - `emoji: string`

        A single emoji. Send an empty string to remove a previous reaction to the same message.

      - `type: "reaction"`

        - `"reaction"`

      - `channel_message_id: optional string`

        Inbound only: the channel's opaque identifier for the message being reacted to. Always present on an inbound reaction, beside message_id whenever the target resolved. Match it literally to Message.channel_message_id in the chat transcript; never parse it or compare it with Message.id.

      - `message_id: optional string`

        This API's Message.id for the message being reacted to. Outbound: required, and the API resolves it into the channel namespace before sending, so the message must be one this account can see in the chat the send addresses, and — for a message this API sent — one the channel has already acknowledged. Inbound: present whenever the target is a message of the same chat, so you can keep only this identifier; absent when the chat does not hold the target.

    - `InteractiveReply object { id, kind, title, 2 more }`

      Inbound only — never sendable. The customer's selection from an interactive_list (kind list_reply) or interactive_buttons (kind button_reply) message, carrying the id you assigned to the chosen row or button.

      - `id: string`

        The id you assigned to the chosen row or button, exactly as you sent it.

      - `kind: "list_reply" or "button_reply"`

        Which interactive message kind was answered.

        - `"list_reply"`

        - `"button_reply"`

      - `title: string`

        The chosen row's or button's visible title.

      - `type: "interactive_reply"`

        - `"interactive_reply"`

      - `description: optional string`

        The chosen list row's secondary text, when it had one.

    - `TemplateReply object { payload, text, type }`

      Inbound only — never sendable. The customer tapped a quick-reply button on an approved template you sent, or a reply button on a card of an interactive_carousel you sent. payload is the value you set on that button: the template's quick_reply slot payload (or the button's label when you left the slot out), or the carousel card button's id. text is the label the customer saw. A carousel tap carries nothing else that identifies the card, so the id you chose is the card's only identity. For a template tap, the message's reply_to_message_id names the template message that carried the button. Distinct from interactive_reply, which answers an interactive_buttons or interactive_list message.

      - `payload: string`

        The quick_reply slot's payload from your send, or the button label when the slot was omitted; for a carousel card, the tapped reply button's id.

      - `text: string`

        The tapped button's label, as the customer saw it.

      - `type: "template_reply"`

        - `"template_reply"`

    - `AddressReply object { type, values, saved_address_id }`

      Inbound only: the structured values submitted in response to an address request.

      - `type: "address_reply"`

        - `"address_reply"`

      - `values: object { address, building_name, city, 8 more }`

        - `address: optional string`

        - `building_name: optional string`

        - `city: optional string`

        - `floor_number: optional string`

        - `house_number: optional string`

        - `landmark_area: optional string`

        - `name: optional string`

        - `phone_number: optional string`

        - `pin_code: optional string`

        - `state: optional string`

        - `tower_number: optional string`

      - `saved_address_id: optional string`

        Identifier of the selected saved address, when one was selected.

    - `Order object { catalog_id, items, type, text }`

      Inbound only — never sendable. An order the customer placed from a product catalog.

      - `catalog_id: string`

        The catalog the ordered items belong to.

      - `items: array of object { currency, price, product_retailer_id, quantity }`

        The ordered items.

        - `currency: string`

          ISO 4217 currency code for price.

        - `price: number`

          The per-item price at order time.

        - `product_retailer_id: string`

          Your identifier for the product, as registered in the catalog.

        - `quantity: number`

      - `type: "order"`

        - `"order"`

      - `text: optional string`

        Free text the customer attached to the order, when any.

    - `Referral object { type, body, click_id, 7 more }`

      Inbound only — never sendable. The ad or post context a customer's first message arrived from (for example an ad whose call to action opens a chat). A referral also opens a free-entry-point customer window — see the chat's customer_window.

      - `type: "referral"`

        - `"referral"`

      - `body: optional string`

        The ad's body text at click time.

      - `click_id: optional string`

        The click identifier assigned by the ad platform, for attribution.

      - `headline: optional string`

        The ad's headline at click time.

      - `image_url: optional string`

        URL of the ad's image creative, when media_kind is image.

      - `media_kind: optional string`

        The ad creative's media kind, e.g. image or video.

      - `source_id: optional string`

        The ad or post id.

      - `source_kind: optional string`

        What the source was, e.g. ad or post.

      - `source_url: optional string`

        The URL of the ad or post the customer came from.

      - `video_url: optional string`

        URL of the ad's video creative, when media_kind is video.

    - `System object { body, type, event }`

      Inbound only — never sendable. A system event in the chat, such as the customer changing their number. New event kinds appear over time; body is always present and human-readable.

      - `body: string`

        Human-readable description of the system event.

      - `type: "system"`

        - `"system"`

      - `event: optional string`

        The system event's kind, when the channel identifies one. New kinds appear over time — treat unknown values as informational.

    - `Unsupported object { raw, type, kind }`

      Inbound only — never sendable. A message kind this API does not yet type natively, carried as a typed passthrough: raw holds the channel payload, so a new message kind is never a black box or a silent drop. Native part types for popular kinds are added over time; this part is the compatibility guarantee in the meantime.

      - `raw: unknown`

        The channel payload — any JSON value: object, array, string, number, boolean, or null. Deliberately unconstrained, because the whole point of this part is to carry a shape this API does not yet know. For rows written by API version 1.3.0 or later, object member order, number formatting and duplicated members survive storage and read-back. The canonical encoder may compact insignificant whitespace before storage. Rows written before API version 1.3.0 retain normalized JSON only: member order and number formatting may differ, and only the last duplicated member survives.

      - `type: "unsupported"`

        - `"unsupported"`

      - `kind: optional string`

        The channel's name for the message kind, when it declares one.

  - `status: string`

    The message's delivery state as last reported.

    Inbound messages read `received`: they arrived, and no delivery of ours
    ran. A message you sent starts `accepted` and moves through `sent`,
    `delivered` and `read` as the channel reports them, with `failed` as the
    definitive negative outcome.

    `unknown` means exactly that: the send is INDETERMINATE and we decline
    to guess. It is NOT terminal — an indeterminate send is never re-sent,
    and it resolves to `sent`, `delivered`, `read` or `failed` when the
    channel's own report arrives — so keep observing rather than treating it
    as an outcome.

    States are reported asynchronously on the chat's event sequence, and the
    set grows additively: treat a value you do not recognise as "no
    information" rather than failing on it.

    Retention of this published status follows the chat transcript policy:
    there is no scheduled pruning, but no fixed minimum availability period
    is published yet. The present absence of a scheduled age-based sweep is
    not a promise of indefinite availability.

    UNDER A `sk_test_` KEY THESE ARE SIMULATED. A test-key send is reported
    `sent` and then `delivered` within milliseconds, every time; `delivered`
    there means a simulator accepted it, not that a device received it. Live
    delivery can lag by hours, can never report `delivered` at all, and can
    fail after the channel accepted the message — so do not calibrate
    timeouts or "delivered means it arrived" logic against a test key.

  - `channel_message_id: optional string`

    The channel's opaque identifier for this message, when the channel has assigned one. An inbound reaction's `channel_message_id` matches this field literally; never parse it or compare it with `id`. It is absent while an accepted outbound message has not yet received a channel identifier, and until it is present an inbound reaction to this message carries only `channel_message_id`.

  - `failure: optional object { code, message }`

    Why a failed message failed, when we have a reason worth publishing.

    Present only on `status` `failed`, and not on every one of those: a
    message can fail for a reason outside the published vocabulary and carry
    no `failure` at all. Its absence means "no published reason", never "no
    reason". Most messages the channel refused do carry one: a specific code
    where one is published, and `channel_refused` for most of the rest; a few
    kinds of refusal still carry none.

    `status` stays the thing to branch on. This is additive detail beside it,
    so an integration written before this field existed still sees a terminal
    `failed` and behaves exactly as it did.

    - `code: string`

      The specific reason.

      `sender_deregistered` — the number this message was going out from is no
      longer a registered sending number, and the message never left. This is
      terminal: it is not a pause and it does not clear, so retrying the same
      message cannot succeed and that conversation is over. Reach this customer
      by starting a new one on another of your numbers with a template, exactly
      as you would any customer whose 24-hour window has closed; `GET /v1/chats`
      shows which number each conversation uses. You are not billed for a
      message that failed this way.

      `quality_hold_expired` — this marketing message remained held by an
      automatic safety control on its sending line for ten minutes and was not
      sent. This is terminal: retrying the same message cannot succeed on that
      conversation. Send marketing from another of your numbers with a template,
      or contact support about the sending line. You are not billed for a message
      that failed this way.

      `recipient_country_restricted` — the business behind the sending number
      is not currently permitted to message customers in this recipient's
      country, so the message was not delivered. This is terminal for the
      message: sending it again cannot succeed while the restriction stands.
      Most often this is because the business has not yet completed business
      verification: complete it, and until then message customers in the
      sending number's own country. If the business is already verified, review
      the messaging policy for the business's category. It can arrive either as
      an immediate `failed` or after the message was first reported `sent`;
      both mean the same thing. You are not billed for a message that failed
      this way.

      `payment_method_required` — the business behind the sending number has
      no working payment method on its messaging account, so this message was
      not sent. This is terminal for the message: retrying it cannot succeed
      until a payment method is added. Add or update the payment method in the
      messaging account's billing settings, then send again as a new message.
      Replies inside an open 24-hour customer window are charged too once the
      sending number's monthly free allowance of them is used up, so they also
      need a working payment method and can fail this way. It can arrive either
      as an immediate `failed` or after the message was first reported `sent`;
      both mean the same thing.
      You are not billed for a message that failed this way.

      `recipient_phone_number_required` — this customer is known only by a
      username identifier, not a phone number, and this message can be
      delivered only to a phone number, so it was not sent. This is terminal
      for the message: sending it again to this customer cannot succeed.
      One-time-code templates (copy code, one-tap or zero-tap autofill) are the
      usual cause; deliver the code another way. Other messages, including
      templates without a one-time-code button, can still be sent to this
      customer. When the template is known in advance to carry a one-time-code
      button, the send is refused at once with the error
      `recipient_phone_number_required` instead and nothing is accepted. It can
      arrive either as an immediate `failed` or after the message was first
      reported `sent`; both mean the same thing. You are not billed for a
      message that failed this way.

      `business_restricted` — this message was refused because the business
      behind the sending number is most likely restricted on its messaging
      account, so it was not sent. This is terminal for the message: sending
      it again will not succeed while the restriction stands. Review the status
      of the business's messaging account, and if the business believes it
      complies with the messaging policies, request a review of the
      restriction. It can arrive either as an immediate `failed` or after the
      message was first reported `sent`; both mean the same thing. You are not
      billed for a message that failed this way.

      `channel_refused` — this message was refused upstream and was not sent,
      for a reason that has no more specific code here, and it will not be
      retried automatically. Contact support with this message's `id` to learn
      the reason and whether sending it again can succeed. It can arrive either as an immediate `failed` or after the message
      was first reported `sent`; both mean the same thing. You are not billed
      for a message that failed this way. A more specific code may be published
      for some of these refusals in a later release; treat a code you do not
      recognise exactly as you treat this one.

    - `message: string`

      Human-readable, written for a person reading it. Do not branch on it.

  - `from: optional string`

    The number this message left by, or arrived on, in E.164 (leading `+`).
    Every message in one conversation carries the same value — a
    conversation never moves to another number. Absent only if the number is
    no longer one of yours.

  - `from_selection: optional object { reason }`

    Present only when this service chose the sending number for you — that
    is, when you omitted `from`. A send that named `from` omits this rather
    than reporting a choice nobody made.

    - `reason: string`

      Why this number was used. Exactly one of:

      - `reused_active_chat` — this customer already had a conversation and it
        continues on the same number.
      - `new_best_number` — there was no existing conversation, so the best
        available number was chosen.

      Like every other vocabulary here it is a plain string that may grow
      additively; treat a value you do not recognise as "chosen for you".

  - `reply_to_message_id: optional string`

    This API's `id` of the earlier message in this chat that this message
    replies to. On an inbound message it is the message the customer quoted;
    on an outbound message it is the `reply_to_message_id` you sent. Look the
    value up in this chat's transcript to show what was answered; it is the
    same namespace as `id`, never a channel identifier.

    Absent when there is no reply context this API can resolve — a message
    that quotes nothing, or one that quotes a message this API never
    recorded in this chat. Treat absence as "no reply context available",
    not as proof the message was not a reply.

  - `zero_retention: optional true`

    True when the message is under zero-day retention: its content is not
    kept, so later reads and idempotent retries return empty `parts`. The
    original send response still carries the content.
    Retention applies only within Linq's systems. The WhatsApp channel
    operator's own processing and retention of message content are governed
    by its terms and are not affected by this setting.

    - `true`

### Message

- `Message object { id, chat_id, created_at, 8 more }`

  One message, in either direction.

  - `id: string`

    This API's message id. Put this value in an outbound reaction's `message_id`; an inbound reaction to this message carries the same value in its `message_id`, so this is the one identifier you need to keep. Mark-as-read targets the latest unread inbound message in a chat, so it takes the chat id.

  - `chat_id: string`

    The chat this message belongs to.

  - `created_at: string`

  - `direction: "inbound" or "outbound"`

    `inbound` (from the customer) or `outbound` (sent by you).

    - `"inbound"`

    - `"outbound"`

  - `parts: array of object { body, type, preview_url }  or object { kind, type, availability, 10 more }  or object { language, name, type, 4 more }  or 17 more`

    The message content — sendable parts, plus the inbound-only types on
    inbound messages. Empty only when `zero_retention` is true, or when an
    idempotent retry returns an ephemeral message whose content retention
    cleanup has removed.

    - `Text object { body, type, preview_url }`

      A plain text message.

      - `body: string`

        The message text. URLs render as tappable links.

      - `type: "text"`

        - `"text"`

      - `preview_url: optional boolean`

        Render a preview card for the first URL in body. Defaults to false.

    - `Media object { kind, type, availability, 10 more }`

      A media message — image, video, audio, document, or sticker. The two identifiers are distinct namespaces: inbound media carries the channel's transient media_id; outbound media carries our durable upload_ref from POST /v1/media.

      - `kind: "image" or "video" or "audio" or 2 more`

        Which kind of media this is. Captions apply to image, video and document only; filename applies to document only.

        - `"image"`

        - `"video"`

        - `"audio"`

        - `"document"`

        - `"sticker"`

      - `type: "media"`

        - `"media"`

      - `availability: optional "available" or "unavailable" or "pending"`

        Response only, inbound parts only: whether the download answers the bytes. available: the file was captured and is retained. unavailable: it is not downloadable now, and unavailable_reason says why; with timed_out the download answers 409 while capture continues and can later answer 200, otherwise it answers 410, or 404 once the file is no longer known. It describes the file, not the exact HTTP answer of a later download, which can also refuse for capacity or authorization. pending: capture has not finished yet; only reads of the conversation return pending, and a message event never does. A message.received event can be recorded once every attachment has been captured or has failed, or ten minutes after the message arrived; an event part without availability may still be capturing, so retry its download as the 409 Retry-After directs. A message event keeps the value it was recorded with; reads return the current one. Historical messages received before it was recorded omit it. Never accepted on sends.

        - `"available"`

        - `"unavailable"`

        - `"pending"`

      - `byte_size: optional number`

        Response only, inbound parts only: the exact size in bytes of the retained attachment, equal to the download's Content-Length. Present only while the bytes are captured and retained — the same lifetime as a working url, ending 30 days after server receipt — so it can be absent from a message event while capture has not completed, and its absence is not final; once present it never changes. Absent rather than zero. Never accepted on sends.

      - `caption: optional string`

        Caption rendered with the media. Image, video and document only — a caption on an audio or sticker part is rejected, never dropped.

      - `filename: optional string`

        Display filename. Documents only — a filename on any other kind is rejected, never dropped.

      - `media_id: optional string`

        Inbound only: the channel's transient media identifier. It expires after seven days and is never an outbound address; an outbound value is rejected with a pointer to media_id and upload_ref as the remedy.

      - `mime_type: optional string`

        Response only, inbound parts only: the media type the channel declared when the message arrived, verbatim. It may carry parameters (for example audio/ogg; codecs=opus) and may differ from the Content-Type the download answers with, which describes the retained bytes. Present on the webhook body and on every later read with the same value. Historical messages received before it was recorded omit this field. Never accepted on sends.

      - `source_url: optional string`

        Outbound only: a public https URL of the bytes to send, as an alternative to upload_ref. Supply exactly one of upload_ref or source_url. The address is fetched once, before the send is accepted, and its exact bytes are stored as an ordinary upload in this account and test/live partition; the accepted message and every later read carry that upload's upload_ref, never the address, and no retry fetches it again. The URL must be absolute https on the default port with no credentials and no fragment, must resolve to a public address (every redirect hop included, at most three), and must answer with the complete bytes and their exact media type within the fetch deadline. The bytes are then held to the same media type and size rules as POST /v1/media for the part's kind; a mismatch is refused, never converted.

      - `unavailable_reason: optional string`

        Response only, present only when availability is unavailable. An open string: the values today are timed_out, capture_failed and expired, and new values may be added. timed_out: the file was still being retrieved ten minutes after the message arrived, so its message event was recorded without it; retrieval continues and the file may become downloadable later, which reads of the conversation report. capture_failed: the file could not be retrieved. expired: the file was retrieved and is no longer retained, or is no longer known (the download then answers 410 or 404). Treat an unknown value as capture_failed. Never accepted on sends.

      - `upload_ref: optional string`

        Outbound only: our durable handle returned by POST /v1/media. Handles visibly begin upload. and are reusable within the account and test/live partition that created them.

      - `url: optional string`

        Response only: stable attachment download URL requiring the account Bearer key on every request. Pending capture returns 409 with Retry-After; retained bytes expire 30 days after server receipt. Historical unbound messages omit this field. Never accepted on sends.

      - `voice: optional boolean`

        Audio only. On a send it is optional: true sends the audio as a voice note; false, or leaving it out, sends it as an ordinary audio file. A voice note needs an upload stored as audio/ogg that holds mono Opus audio, sent by upload_ref or source_url within the existing audio size limit. The bytes are sent as supplied: they are never converted or inspected, so encode them as mono Opus before uploading. The recipient sees a play button only when the voice file is 512 KB or smaller; a larger voice note is still delivered but must be downloaded before it plays. Supported on sends to individual chats only. On incoming audio parts it is response-only: whether the channel reported the audio as a voice note, verbatim. true means it was recorded with the messaging client's voice-recording feature; false means it was sent as an audio file. Present on the webhook body and on every later read with the same value. Absent on an incoming part means the channel did not say, never that the audio is not a voice note. Other kinds never carry it, and historical messages received before it was recorded omit it.

    - `Template object { language, name, type, 4 more }`

      An approved message template — the one part type sendable outside an open customer service window. name and language identify the approved template; parameters fills its named placeholders. Authentication templates use this same generic part: their published parameter schema names exactly one parameter, `code` — the one-time code you generate, 1–15 characters — and the message shows it in its body and behind its code button. Every sendable template is registered and approved ahead of the send and publishes its own parameter schema — a send referencing an unknown template, a missing or unknown parameter, or a value that breaks the template's rules is rejected with a 422 naming the exact field.

      - `language: string`

        The template's language-and-locale code, e.g. en_US.

      - `name: string`

        The approved template's name. Lowercase letters, digits and underscores only.

      - `type: "template"`

        - `"template"`

      - `buttons: optional array of object { index, payload, type }`

        Send-time values for this approved template's own dynamic buttons, in ascending index order: a url button whose destination ends in {{1}} takes the value appended there and is REQUIRED; a quick_reply button takes the payload returned to you when it is tapped and is OPTIONAL — omit it and the button returns its label. Fixed buttons have no slot. The approved template's send_schema lists the (index, type) slots and an x-rule stating which are required; omit buttons entirely for a template with no url slot when the labels are what you want back.

        - `index: number`

        - `payload: string`

        - `type: "quick_reply" or "url"`

          - `"quick_reply"`

          - `"url"`

      - `cards: optional array of object { card_index, kind, upload_ref, 2 more }`

        Per-card values for an approved media-card carousel. The approved template's send_schema provides structural bounds and an x-rule listing the exact card order, media kind, named body values, and dynamic button slots enforced by runtime L2 validation. Omit for non-carousel templates.

        - `card_index: number`

        - `kind: "image" or "video"`

          - `"image"`

          - `"video"`

        - `upload_ref: string`

          A durable handle returned by POST /v1/media. It is resolved within this key's account and partition, then consumed into a transient channel media identifier only at delivery.

        - `buttons: optional array of object { index, payload, type }`

          - `index: number`

          - `payload: string`

          - `type: "quick_reply" or "url"`

            - `"quick_reply"`

            - `"url"`

        - `parameters: optional unknown`

          Named values for this card's body placeholders.

      - `header: optional object { kind, upload_ref }`

        The uploaded media used for this approved template's media header. Omit unless this template's send_schema requires it.

        - `kind: "image" or "video" or "document"`

          - `"image"`

          - `"video"`

          - `"document"`

        - `upload_ref: string`

          A durable handle returned by POST /v1/media.

      - `parameters: optional unknown`

        Named values for the template's placeholders. The shape is per-template: each registered template publishes its own parameter schema — hand THAT schema to your model when composing a specific template. Omit for templates with no placeholders.

    - `InteractiveList object { body, button, sections, 3 more }`

      An interactive list message: body text plus a button that opens a sectioned list of selectable rows. The customer's selection arrives as an inbound interactive_reply part with kind list_reply, carrying the chosen row's id.

      - `body: string`

        The message body.

      - `button: string`

        Label of the button that opens the list.

      - `sections: array of object { rows, title }`

        The list's sections, each holding selectable rows. At most 10 rows in total across ALL sections combined — not 10 per section.

        - `rows: array of object { id, title, description }`

          This section's rows. The 10-row limit is on the TOTAL across all sections, so a second section reduces what this one may hold.

          - `id: string`

            Your identifier for the row — echoed back as the interactive_reply's id.

          - `title: string`

            The row's visible title.

          - `description: optional string`

            Secondary text under the title.

        - `title: optional string`

          Section title. Required when the list has more than one section.

      - `type: "interactive_list"`

        - `"interactive_list"`

      - `footer: optional string`

        Footer text below the body.

      - `header: optional string`

        Header text above the body.

    - `InteractiveButtons object { body, buttons, type, 3 more }`

      An interactive reply-buttons message: body text plus up to three tappable buttons. The customer's tap arrives as an inbound interactive_reply part with kind button_reply, carrying the tapped button's id.

      - `body: string`

        The message body.

      - `buttons: array of object { id, title }`

        The tappable buttons — at most three.

        - `id: string`

          Your identifier for the button — echoed back as the interactive_reply's id.

        - `title: string`

          The button's visible label.

      - `type: "interactive_buttons"`

        - `"interactive_buttons"`

      - `footer: optional string`

        Footer text below the body.

      - `header: optional string`

        Header text above the body.

      - `media_header: optional object { kind, upload_ref }`

        Uploaded image, video, or document above the body. Use either header or media_header, never both.

        - `kind: "image" or "video" or "document"`

          - `"image"`

          - `"video"`

          - `"document"`

        - `upload_ref: string`

          A durable handle returned by POST /v1/media.

    - `InteractiveCtaURL object { body, display_text, type, 4 more }`

      An interactive call-to-action message: body text plus one button that opens a URL.

      - `body: string`

        The message body.

      - `display_text: string`

        The button's visible label.

      - `type: "interactive_cta_url"`

        - `"interactive_cta_url"`

      - `url: string`

        The URL the button opens. Must be absolute, with an http or https scheme.

      - `footer: optional string`

        Footer text below the body.

      - `header: optional string`

        Header text above the body.

      - `media_header: optional object { kind, upload_ref, filename }`

        Uploaded image, video, or document above the body. Use either header or media_header, never both.

        - `kind: "image" or "video" or "document"`

          - `"image"`

          - `"video"`

          - `"document"`

        - `upload_ref: string`

          A durable handle returned by POST /v1/media.

        - `filename: optional string`

          The file name the customer sees on a document header. Documents only.

    - `InteractiveCarousel object { body, cards, type }`

      An interactive carousel: one message whose body sits above 2 to 10 horizontally scrolling cards. Each card shows an uploaded image, or a video from a public link, and either one button that opens a URL or reply buttons. A tap on a card's reply button arrives as an inbound template_reply part whose payload is that button's id; nothing else in the reply identifies the card, so the id you choose is the card's identity.

      - `body: string`

        The message text shown above the cards.

      - `cards: array of object { card_index, header, body, 2 more }`

        The cards, left to right.

        - `card_index: number`

          This card's zero-based position: the first card is 0, the next 1, and so on, in array order.

        - `header: object { kind, upload_ref, url }`

          The card's media: an image named by upload_ref, or a video named by the public https url it plays from. Every card carries the same kind.

          - `kind: "image" or "video"`

            - `"image"`

            - `"video"`

          - `upload_ref: optional string`

            A durable handle returned by POST /v1/media for an image upload. Image cards only.

          - `url: optional string`

            The public https address the card's video plays from. Video cards only. It is sent exactly as written and never fetched by this API, so it must be publicly reachable and serve a playable video; a link that does not play is accepted and then reported as a failed delivery (message.failed with channel_refused).

        - `body: optional string`

          Optional text below the card's image or video, with at most two line breaks.

        - `reply_buttons: optional array of object { id, title }`

          Buttons the customer taps to reply. Use either url_button or reply_buttons on every card.

          - `id: string`

            Your identifier for the button, echoed back as the template_reply's payload. Unique across the whole carousel.

          - `title: string`

            The button's visible label.

        - `url_button: optional object { display_text, url }`

          One button that opens a URL. Use either url_button or reply_buttons on every card.

          - `display_text: string`

            The button's visible label.

          - `url: string`

            The absolute https URL the button opens.

      - `type: "interactive_carousel"`

        - `"interactive_carousel"`

    - `LocationRequest object { body, type }`

      Asks the customer to share a location. The response arrives as a location part.

      - `body: string`

        Prompt shown above the send-location button.

      - `type: "location_request"`

        - `"location_request"`

    - `ContactRequest object { body, type }`

      Asks the customer to share their own contact card with one tap. Useful when you know the customer only by a username identifier and need a phone number. The answer arrives as an ordinary contacts part whose card carries origin contact_request; a card the customer shares by hand carries origin other instead. The answer is not linked to your request as a reply: its message carries no reply_to_message_id, so match it by origin and timing. Sendable inside the customer window, to individual chats only.

      - `body: string`

        Prompt shown above the share-contact button.

      - `type: "contact_request"`

        - `"contact_request"`

    - `AddressRequest object { body, country, type }`

      Reserved for asking an eligible customer in India to submit a structured address. Sending this part is not available until business and customer eligibility can be verified at acceptance. Address responses already arrive as address_reply parts.

      - `body: string`

        Prompt shown above the address form.

      - `country: "IN"`

        ISO country code. Structured address requests are currently available only in India.

        - `"IN"`

      - `type: "address_request"`

        - `"address_request"`

    - `Contacts object { contacts, type }`

      One or more contact cards.

      - `contacts: array of object { name, addresses, birthday, 5 more }`

        The contact cards to send. The channel permits far more; this API caps a message at five, because a message carrying hundreds of cards is a mistake rather than a use case.

        - `name: object { formatted_name, first_name, last_name, 3 more }`

          - `formatted_name: string`

            The contact's full display name.

          - `first_name: optional string`

          - `last_name: optional string`

          - `middle_name: optional string`

          - `prefix: optional string`

          - `suffix: optional string`

        - `addresses: optional array of object { city, country, country_code, 4 more }`

          - `city: optional string`

          - `country: optional string`

          - `country_code: optional string`

          - `kind: optional string`

            A label for the address, e.g. WORK, HOME.

          - `state: optional string`

          - `street: optional string`

          - `zip: optional string`

        - `birthday: optional string`

          The contact's birthday as YYYY-MM-DD.

        - `emails: optional array of object { email, kind }`

          - `email: string`

          - `kind: optional string`

            A label for the address, e.g. WORK, HOME.

        - `org: optional object { company, department, title }`

          - `company: optional string`

          - `department: optional string`

          - `title: optional string`

        - `origin: optional string`

          Response-only: how the customer came to share this card, as the channel reported it, verbatim. contact_request means they tapped the share button on a contact_request you sent; other means they shared a card themselves. New values may appear, so treat any other value as unrecognised rather than as either of these. Absent when the channel did not say, and on cards received before it was recorded. A send carrying it is rejected with a pointer at the member.

        - `phones: optional array of object { phone, kind, messaging_id }`

          - `phone: string`

            The phone number, ideally in +E.164 form.

          - `kind: optional string`

            A label for the number, e.g. CELL, MAIN, WORK, HOME.

          - `messaging_id: optional string`

            The contact's messaging-account identifier on this channel, when known — makes the card openable in the messaging app.

        - `urls: optional array of object { url, kind }`

          - `url: string`

          - `kind: optional string`

            A label for the URL, e.g. WORK, HOME.

      - `type: "contacts"`

        - `"contacts"`

    - `Location object { latitude, longitude, type, 2 more }`

      A location pin.

      - `latitude: number`

        Latitude in decimal degrees.

      - `longitude: number`

        Longitude in decimal degrees.

      - `type: "location"`

        - `"location"`

      - `address: optional string`

        The place's address, shown under the name.

      - `name: optional string`

        The place's name, shown on the pin.

    - `Reaction object { emoji, type, channel_message_id, message_id }`

      An emoji reaction to an earlier message in the chat. message_id is this API's Message.id for the target in both directions: it is what you send, and it is appended to an inbound reaction whenever the target is a message of the same chat. channel_message_id is the channel's opaque identifier and appears on inbound reactions only.

      - `emoji: string`

        A single emoji. Send an empty string to remove a previous reaction to the same message.

      - `type: "reaction"`

        - `"reaction"`

      - `channel_message_id: optional string`

        Inbound only: the channel's opaque identifier for the message being reacted to. Always present on an inbound reaction, beside message_id whenever the target resolved. Match it literally to Message.channel_message_id in the chat transcript; never parse it or compare it with Message.id.

      - `message_id: optional string`

        This API's Message.id for the message being reacted to. Outbound: required, and the API resolves it into the channel namespace before sending, so the message must be one this account can see in the chat the send addresses, and — for a message this API sent — one the channel has already acknowledged. Inbound: present whenever the target is a message of the same chat, so you can keep only this identifier; absent when the chat does not hold the target.

    - `InteractiveReply object { id, kind, title, 2 more }`

      Inbound only — never sendable. The customer's selection from an interactive_list (kind list_reply) or interactive_buttons (kind button_reply) message, carrying the id you assigned to the chosen row or button.

      - `id: string`

        The id you assigned to the chosen row or button, exactly as you sent it.

      - `kind: "list_reply" or "button_reply"`

        Which interactive message kind was answered.

        - `"list_reply"`

        - `"button_reply"`

      - `title: string`

        The chosen row's or button's visible title.

      - `type: "interactive_reply"`

        - `"interactive_reply"`

      - `description: optional string`

        The chosen list row's secondary text, when it had one.

    - `TemplateReply object { payload, text, type }`

      Inbound only — never sendable. The customer tapped a quick-reply button on an approved template you sent, or a reply button on a card of an interactive_carousel you sent. payload is the value you set on that button: the template's quick_reply slot payload (or the button's label when you left the slot out), or the carousel card button's id. text is the label the customer saw. A carousel tap carries nothing else that identifies the card, so the id you chose is the card's only identity. For a template tap, the message's reply_to_message_id names the template message that carried the button. Distinct from interactive_reply, which answers an interactive_buttons or interactive_list message.

      - `payload: string`

        The quick_reply slot's payload from your send, or the button label when the slot was omitted; for a carousel card, the tapped reply button's id.

      - `text: string`

        The tapped button's label, as the customer saw it.

      - `type: "template_reply"`

        - `"template_reply"`

    - `AddressReply object { type, values, saved_address_id }`

      Inbound only: the structured values submitted in response to an address request.

      - `type: "address_reply"`

        - `"address_reply"`

      - `values: object { address, building_name, city, 8 more }`

        - `address: optional string`

        - `building_name: optional string`

        - `city: optional string`

        - `floor_number: optional string`

        - `house_number: optional string`

        - `landmark_area: optional string`

        - `name: optional string`

        - `phone_number: optional string`

        - `pin_code: optional string`

        - `state: optional string`

        - `tower_number: optional string`

      - `saved_address_id: optional string`

        Identifier of the selected saved address, when one was selected.

    - `Order object { catalog_id, items, type, text }`

      Inbound only — never sendable. An order the customer placed from a product catalog.

      - `catalog_id: string`

        The catalog the ordered items belong to.

      - `items: array of object { currency, price, product_retailer_id, quantity }`

        The ordered items.

        - `currency: string`

          ISO 4217 currency code for price.

        - `price: number`

          The per-item price at order time.

        - `product_retailer_id: string`

          Your identifier for the product, as registered in the catalog.

        - `quantity: number`

      - `type: "order"`

        - `"order"`

      - `text: optional string`

        Free text the customer attached to the order, when any.

    - `Referral object { type, body, click_id, 7 more }`

      Inbound only — never sendable. The ad or post context a customer's first message arrived from (for example an ad whose call to action opens a chat). A referral also opens a free-entry-point customer window — see the chat's customer_window.

      - `type: "referral"`

        - `"referral"`

      - `body: optional string`

        The ad's body text at click time.

      - `click_id: optional string`

        The click identifier assigned by the ad platform, for attribution.

      - `headline: optional string`

        The ad's headline at click time.

      - `image_url: optional string`

        URL of the ad's image creative, when media_kind is image.

      - `media_kind: optional string`

        The ad creative's media kind, e.g. image or video.

      - `source_id: optional string`

        The ad or post id.

      - `source_kind: optional string`

        What the source was, e.g. ad or post.

      - `source_url: optional string`

        The URL of the ad or post the customer came from.

      - `video_url: optional string`

        URL of the ad's video creative, when media_kind is video.

    - `System object { body, type, event }`

      Inbound only — never sendable. A system event in the chat, such as the customer changing their number. New event kinds appear over time; body is always present and human-readable.

      - `body: string`

        Human-readable description of the system event.

      - `type: "system"`

        - `"system"`

      - `event: optional string`

        The system event's kind, when the channel identifies one. New kinds appear over time — treat unknown values as informational.

    - `Unsupported object { raw, type, kind }`

      Inbound only — never sendable. A message kind this API does not yet type natively, carried as a typed passthrough: raw holds the channel payload, so a new message kind is never a black box or a silent drop. Native part types for popular kinds are added over time; this part is the compatibility guarantee in the meantime.

      - `raw: unknown`

        The channel payload — any JSON value: object, array, string, number, boolean, or null. Deliberately unconstrained, because the whole point of this part is to carry a shape this API does not yet know. For rows written by API version 1.3.0 or later, object member order, number formatting and duplicated members survive storage and read-back. The canonical encoder may compact insignificant whitespace before storage. Rows written before API version 1.3.0 retain normalized JSON only: member order and number formatting may differ, and only the last duplicated member survives.

      - `type: "unsupported"`

        - `"unsupported"`

      - `kind: optional string`

        The channel's name for the message kind, when it declares one.

  - `status: string`

    The message's delivery state as last reported.

    Inbound messages read `received`: they arrived, and no delivery of ours
    ran. A message you sent starts `accepted` and moves through `sent`,
    `delivered` and `read` as the channel reports them, with `failed` as the
    definitive negative outcome.

    `unknown` means exactly that: the send is INDETERMINATE and we decline
    to guess. It is NOT terminal — an indeterminate send is never re-sent,
    and it resolves to `sent`, `delivered`, `read` or `failed` when the
    channel's own report arrives — so keep observing rather than treating it
    as an outcome.

    States are reported asynchronously on the chat's event sequence, and the
    set grows additively: treat a value you do not recognise as "no
    information" rather than failing on it.

    Retention of this published status follows the chat transcript policy:
    there is no scheduled pruning, but no fixed minimum availability period
    is published yet. The present absence of a scheduled age-based sweep is
    not a promise of indefinite availability.

    UNDER A `sk_test_` KEY THESE ARE SIMULATED. A test-key send is reported
    `sent` and then `delivered` within milliseconds, every time; `delivered`
    there means a simulator accepted it, not that a device received it. Live
    delivery can lag by hours, can never report `delivered` at all, and can
    fail after the channel accepted the message — so do not calibrate
    timeouts or "delivered means it arrived" logic against a test key.

  - `channel_message_id: optional string`

    The channel's opaque identifier for this message, when the channel has assigned one. An inbound reaction's `channel_message_id` matches this field literally; never parse it or compare it with `id`. It is absent while an accepted outbound message has not yet received a channel identifier, and until it is present an inbound reaction to this message carries only `channel_message_id`.

  - `failure: optional object { code, message }`

    Why a failed message failed, when we have a reason worth publishing.

    Present only on `status` `failed`, and not on every one of those: a
    message can fail for a reason outside the published vocabulary and carry
    no `failure` at all. Its absence means "no published reason", never "no
    reason". Most messages the channel refused do carry one: a specific code
    where one is published, and `channel_refused` for most of the rest; a few
    kinds of refusal still carry none.

    `status` stays the thing to branch on. This is additive detail beside it,
    so an integration written before this field existed still sees a terminal
    `failed` and behaves exactly as it did.

    - `code: string`

      The specific reason.

      `sender_deregistered` — the number this message was going out from is no
      longer a registered sending number, and the message never left. This is
      terminal: it is not a pause and it does not clear, so retrying the same
      message cannot succeed and that conversation is over. Reach this customer
      by starting a new one on another of your numbers with a template, exactly
      as you would any customer whose 24-hour window has closed; `GET /v1/chats`
      shows which number each conversation uses. You are not billed for a
      message that failed this way.

      `quality_hold_expired` — this marketing message remained held by an
      automatic safety control on its sending line for ten minutes and was not
      sent. This is terminal: retrying the same message cannot succeed on that
      conversation. Send marketing from another of your numbers with a template,
      or contact support about the sending line. You are not billed for a message
      that failed this way.

      `recipient_country_restricted` — the business behind the sending number
      is not currently permitted to message customers in this recipient's
      country, so the message was not delivered. This is terminal for the
      message: sending it again cannot succeed while the restriction stands.
      Most often this is because the business has not yet completed business
      verification: complete it, and until then message customers in the
      sending number's own country. If the business is already verified, review
      the messaging policy for the business's category. It can arrive either as
      an immediate `failed` or after the message was first reported `sent`;
      both mean the same thing. You are not billed for a message that failed
      this way.

      `payment_method_required` — the business behind the sending number has
      no working payment method on its messaging account, so this message was
      not sent. This is terminal for the message: retrying it cannot succeed
      until a payment method is added. Add or update the payment method in the
      messaging account's billing settings, then send again as a new message.
      Replies inside an open 24-hour customer window are charged too once the
      sending number's monthly free allowance of them is used up, so they also
      need a working payment method and can fail this way. It can arrive either
      as an immediate `failed` or after the message was first reported `sent`;
      both mean the same thing.
      You are not billed for a message that failed this way.

      `recipient_phone_number_required` — this customer is known only by a
      username identifier, not a phone number, and this message can be
      delivered only to a phone number, so it was not sent. This is terminal
      for the message: sending it again to this customer cannot succeed.
      One-time-code templates (copy code, one-tap or zero-tap autofill) are the
      usual cause; deliver the code another way. Other messages, including
      templates without a one-time-code button, can still be sent to this
      customer. When the template is known in advance to carry a one-time-code
      button, the send is refused at once with the error
      `recipient_phone_number_required` instead and nothing is accepted. It can
      arrive either as an immediate `failed` or after the message was first
      reported `sent`; both mean the same thing. You are not billed for a
      message that failed this way.

      `business_restricted` — this message was refused because the business
      behind the sending number is most likely restricted on its messaging
      account, so it was not sent. This is terminal for the message: sending
      it again will not succeed while the restriction stands. Review the status
      of the business's messaging account, and if the business believes it
      complies with the messaging policies, request a review of the
      restriction. It can arrive either as an immediate `failed` or after the
      message was first reported `sent`; both mean the same thing. You are not
      billed for a message that failed this way.

      `channel_refused` — this message was refused upstream and was not sent,
      for a reason that has no more specific code here, and it will not be
      retried automatically. Contact support with this message's `id` to learn
      the reason and whether sending it again can succeed. It can arrive either as an immediate `failed` or after the message
      was first reported `sent`; both mean the same thing. You are not billed
      for a message that failed this way. A more specific code may be published
      for some of these refusals in a later release; treat a code you do not
      recognise exactly as you treat this one.

    - `message: string`

      Human-readable, written for a person reading it. Do not branch on it.

  - `from: optional string`

    The number this message left by, or arrived on, in E.164 (leading `+`).
    Every message in one conversation carries the same value — a
    conversation never moves to another number. Absent only if the number is
    no longer one of yours.

  - `reply_to_message_id: optional string`

    This API's `id` of the earlier message in this chat that this message
    replies to. On an inbound message it is the message the customer quoted;
    on an outbound message it is the `reply_to_message_id` you sent. Look the
    value up in this chat's transcript to show what was answered; it is the
    same namespace as `id`, never a channel identifier.

    Absent when there is no reply context this API can resolve — a message
    that quotes nothing, or one that quotes a message this API never
    recorded in this chat. Treat absence as "no reply context available",
    not as proof the message was not a reply.

  - `zero_retention: optional true`

    True when the message is under zero-day retention: its content is not
    kept, so later reads and idempotent retries return empty `parts`. The
    original send response still carries the content.
    Retention applies only within Linq's systems. The WhatsApp channel
    operator's own processing and retention of message content are governed
    by its terms and are not affected by this setting.

    - `true`
