Send a message
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.
Body ParametersJSON
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.
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.
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.
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 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"
}
}