Skip to content
Linq Copy agent prompt

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 error.type api_error; 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 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 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; 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; 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. 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. A daily ceiling stops template traffic while service-window replies remain available; the monthly account ceiling is hard and stops every send at the ceiling.

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 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 ParametersExpand Collapse
"Idempotency-Key": optional string
maxLength255
minLength1
Body ParametersJSONExpand Collapse
parts: array of object { body, type, preview_url } or object { kind, type, caption, 4 more } or object { language, name, type, 3 more } or 8 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); 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.

reaction is declared but not sendable yet. It is refused at accept with HTTP 422 (part_not_supported) naming the part. Media is sendable after its bytes are uploaded with POST /v1/media; outbound parts use that operation’s upload_ref, never 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 and the remedy in the message.

One of the following:
Text object { body, type, preview_url }

A plain text message.

body: string

The message text. URLs render as tappable links.

maxLength4096
type: "text"
preview_url: optional boolean

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

Media object { kind, type, caption, 4 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.

One of the following:
"image"
"video"
"audio"
"document"
"sticker"
type: "media"
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.

maxLength1024
filename: optional string

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

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

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

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

maxLength2048
Template object { language, name, type, 3 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 unchanged: the channel supplies the OTP substitution, so do not invent or pass a code parameter unless the template’s published parameter schema explicitly names one. 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.

maxLength32
name: string

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

maxLength512
type: "template"
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"
One of the following:
"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.

maxLength256
buttons: optional array of object { index, payload, type }
index: number
payload: string
maxLength256
type: "quick_reply" or "url"
One of the following:
"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"
One of the following:
"image"
"video"
"document"
upload_ref: string

A durable handle returned by POST /v1/media.

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

maxLength1024
button: string

Label of the button that opens the list.

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

maxLength200
title: string

The row’s visible title.

maxLength24
description: optional string

Secondary text under the title.

maxLength72
title: optional string

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

maxLength24
type: "interactive_list"
header: optional string

Header text above the body.

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

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

maxLength256
title: string

The button’s visible label.

maxLength20
type: "interactive_buttons"
header: optional string

Header text above the body.

maxLength60
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"
One of the following:
"image"
"video"
"document"
upload_ref: string

A durable handle returned by POST /v1/media.

maxLength256
InteractiveCtaURL object { body, display_text, type, 3 more }

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

body: string

The message body.

maxLength1024
display_text: string

The button’s visible label.

maxLength20
type: "interactive_cta_url"
url: string

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

maxLength2000
header: optional string

Header text above the body.

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

maxLength1024
type: "location_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.

maxLength1024
country: "IN"

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

type: "address_request"
Contacts object { contacts, type }

One or more contact cards.

contacts: array of object { name, addresses, birthday, 4 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.

maxLength512
first_name: optional string
maxLength256
last_name: optional string
maxLength256
middle_name: optional string
maxLength256
prefix: optional string
maxLength256
suffix: optional string
maxLength256
addresses: optional array of object { city, country, country_code, 4 more }
city: optional string
maxLength256
country: optional string
maxLength256
country_code: optional string
maxLength64
kind: optional string

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

maxLength64
state: optional string
maxLength256
street: optional string
maxLength256
zip: optional string
maxLength256
birthday: optional string

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

emails: optional array of object { email, kind }
email: string
maxLength256
kind: optional string

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

maxLength64
org: optional object { company, department, title }
company: optional string
maxLength256
department: optional string
maxLength256
title: optional string
maxLength256
phones: optional array of object { phone, kind, messaging_id }
phone: string

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

maxLength256
kind: optional string

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

maxLength64
messaging_id: optional string

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

maxLength256
urls: optional array of object { url, kind }
url: string
maxLength256
kind: optional string

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

maxLength64
type: "contacts"
Location object { latitude, longitude, type, 2 more }

A location pin.

latitude: number

Latitude in decimal degrees.

maximum90
minimum-90
longitude: number

Longitude in decimal degrees.

maximum180
minimum-180
type: "location"
address: optional string

The place’s address, shown under the name.

maxLength1000
name: optional string

The place’s name, shown on the pin.

maxLength1000
Reaction object { emoji, type, channel_message_id, message_id }

An emoji reaction to an earlier message in the chat. The two identifiers are distinct namespaces: inbound reactions carry the channel’s opaque channel_message_id; outbound reactions will carry this API’s message_id once sending them becomes available.

emoji: string

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

maxLength32
type: "reaction"
channel_message_id: optional string

Inbound only: the channel’s opaque identifier for the message being reacted to. Match it literally to Message.channel_message_id in the chat transcript; never parse it or compare it with Message.id.

maxLength128
message_id: optional string

Outbound only: this API’s Message.id for the message being reacted to. Outbound reactions are not available yet; when they land, the API resolves this identifier into the channel namespace before sending.

maxLength256
to: string

The customer to message, in the channel’s own address form: the same digits-only value the channel reports on inbound messages (country code first, no + and no punctuation). On a conversation that already exists, this is exactly the value that chat’s customer field carries — copy it rather than reconstructing one.

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.

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) if it is not one of your numbers at all, and from_not_sendable (422) 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, 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.

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. Omit the field entirely.

formatdate-time
ReturnsExpand Collapse
AcceptedMessage object { id, chat_id, created_at, 7 more }

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

id: string

This API’s message id. Once outbound reactions are available, put this value in their message_id. 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
formatdate-time
direction: "inbound" or "outbound"

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

One of the following:
"inbound"
"outbound"
parts: array of object { body, type, preview_url } or object { kind, type, caption, 4 more } or object { language, name, type, 3 more } or 14 more

The message content — sendable parts, plus the inbound-only types on inbound messages. Every message carries at least one part.

One of the following:
Text object { body, type, preview_url }

A plain text message.

body: string

The message text. URLs render as tappable links.

maxLength4096
type: "text"
preview_url: optional boolean

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

Media object { kind, type, caption, 4 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.

One of the following:
"image"
"video"
"audio"
"document"
"sticker"
type: "media"
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.

maxLength1024
filename: optional string

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

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

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

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

maxLength2048
Template object { language, name, type, 3 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 unchanged: the channel supplies the OTP substitution, so do not invent or pass a code parameter unless the template’s published parameter schema explicitly names one. 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.

maxLength32
name: string

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

maxLength512
type: "template"
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"
One of the following:
"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.

maxLength256
buttons: optional array of object { index, payload, type }
index: number
payload: string
maxLength256
type: "quick_reply" or "url"
One of the following:
"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"
One of the following:
"image"
"video"
"document"
upload_ref: string

A durable handle returned by POST /v1/media.

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

maxLength1024
button: string

Label of the button that opens the list.

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

maxLength200
title: string

The row’s visible title.

maxLength24
description: optional string

Secondary text under the title.

maxLength72
title: optional string

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

maxLength24
type: "interactive_list"
header: optional string

Header text above the body.

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

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

maxLength256
title: string

The button’s visible label.

maxLength20
type: "interactive_buttons"
header: optional string

Header text above the body.

maxLength60
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"
One of the following:
"image"
"video"
"document"
upload_ref: string

A durable handle returned by POST /v1/media.

maxLength256
InteractiveCtaURL object { body, display_text, type, 3 more }

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

body: string

The message body.

maxLength1024
display_text: string

The button’s visible label.

maxLength20
type: "interactive_cta_url"
url: string

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

maxLength2000
header: optional string

Header text above the body.

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

maxLength1024
type: "location_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.

maxLength1024
country: "IN"

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

type: "address_request"
Contacts object { contacts, type }

One or more contact cards.

contacts: array of object { name, addresses, birthday, 4 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.

maxLength512
first_name: optional string
maxLength256
last_name: optional string
maxLength256
middle_name: optional string
maxLength256
prefix: optional string
maxLength256
suffix: optional string
maxLength256
addresses: optional array of object { city, country, country_code, 4 more }
city: optional string
maxLength256
country: optional string
maxLength256
country_code: optional string
maxLength64
kind: optional string

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

maxLength64
state: optional string
maxLength256
street: optional string
maxLength256
zip: optional string
maxLength256
birthday: optional string

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

emails: optional array of object { email, kind }
email: string
maxLength256
kind: optional string

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

maxLength64
org: optional object { company, department, title }
company: optional string
maxLength256
department: optional string
maxLength256
title: optional string
maxLength256
phones: optional array of object { phone, kind, messaging_id }
phone: string

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

maxLength256
kind: optional string

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

maxLength64
messaging_id: optional string

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

maxLength256
urls: optional array of object { url, kind }
url: string
maxLength256
kind: optional string

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

maxLength64
type: "contacts"
Location object { latitude, longitude, type, 2 more }

A location pin.

latitude: number

Latitude in decimal degrees.

maximum90
minimum-90
longitude: number

Longitude in decimal degrees.

maximum180
minimum-180
type: "location"
address: optional string

The place’s address, shown under the name.

maxLength1000
name: optional string

The place’s name, shown on the pin.

maxLength1000
Reaction object { emoji, type, channel_message_id, message_id }

An emoji reaction to an earlier message in the chat. The two identifiers are distinct namespaces: inbound reactions carry the channel’s opaque channel_message_id; outbound reactions will carry this API’s message_id once sending them becomes available.

emoji: string

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

maxLength32
type: "reaction"
channel_message_id: optional string

Inbound only: the channel’s opaque identifier for the message being reacted to. Match it literally to Message.channel_message_id in the chat transcript; never parse it or compare it with Message.id.

maxLength128
message_id: optional string

Outbound only: this API’s Message.id for the message being reacted to. Outbound reactions are not available yet; when they land, the API resolves this identifier into the channel namespace before sending.

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

maxLength200
kind: "list_reply" or "button_reply"

Which interactive message kind was answered.

One of the following:
"list_reply"
"button_reply"
title: string

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

maxLength24
type: "interactive_reply"
description: optional string

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

maxLength72
AddressReply object { type, values, saved_address_id }

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

type: "address_reply"
values: object { address, building_name, city, 8 more }
address: optional string
maxLength1024
building_name: optional string
maxLength1024
city: optional string
maxLength1024
floor_number: optional string
maxLength1024
house_number: optional string
maxLength1024
landmark_area: optional string
maxLength1024
name: optional string
maxLength1024
phone_number: optional string
maxLength64
pin_code: optional string
maxLength6
state: optional string
maxLength1024
tower_number: optional string
maxLength1024
saved_address_id: optional string

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

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

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

The ordered items.

currency: string

ISO 4217 currency code for price.

maxLength8
price: number

The per-item price at order time.

minimum0
product_retailer_id: string

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

maxLength256
quantity: number
minimum0
type: "order"
text: optional string

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

maxLength4096
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"
body: optional string

The ad’s body text at click time.

maxLength4096
click_id: optional string

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

maxLength256
headline: optional string

The ad’s headline at click time.

maxLength4096
image_url: optional string

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

maxLength2048
media_kind: optional string

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

maxLength64
source_id: optional string

The ad or post id.

maxLength256
source_kind: optional string

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

maxLength64
source_url: optional string

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

maxLength2048
video_url: optional string

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

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

maxLength4096
type: "system"
event: optional string

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

maxLength64
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"
kind: optional string

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

maxLength64
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 during beta, but there is no fixed minimum availability guarantee. 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. Match an inbound reaction’s channel_message_id to 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.

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: not every channel refusal has a reason in the published vocabulary, so a message that failed on the wire may carry no failure at all. Its absence means “no published reason”, never “no reason”.

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.

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

Send a message

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"
        }'
{
  "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"
  }
}
Returns Examples
{
  "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"
  }
}