Messages
هذا المحتوى غير متوفر بلغتك بعد.
POST /v1/messages
Section titled “POST /v1/messages”Sends one message.
Scope: messages:send · Accepts Idempotency-Key
One endpoint handles every message type, discriminated by type, following the
shape of Meta’s own Cloud API so a developer who has seen that already knows
this one.
| Field | Type | Notes |
|---|---|---|
to | string | Phone number in international format. A contact is created if none exists. |
contact_id | string | Address an existing contact instead. One of to or contact_id. |
from | string | Which WhatsApp number to send from. Defaults to the template’s, then the contact’s, then the organization default. |
type | string | text, template, image, video, audio or document. |
text | object | { "body": "..." } when type is text. |
template | object | See below. |
image / video / audio / document | object | See below. |
reference | string | Template sends. Your own id for the message, up to 255 characters. Comes back as in_reply_to.reference when a button on it is tapped. |
callback_url | string | Template sends. Receives the button.reply event for taps on this message, with no webhook to set up. Public http or https; loopback, private and link-local hosts are refused. Not signed. |
bot_reply | boolean | Template sends. Defaults to true. false keeps replies to this message away from the chatbot and AI. They are still stored, shown in the inbox and delivered to webhooks. |
Unknown fields are rejected with 400, so a typo tells you the field name is
wrong rather than that a required one is missing.
{ "to": "+966500000000", "type": "text", "text": { "body": "Your order has shipped." }}Template
Section titled “Template”{ "to": "+966500000000", "type": "template", "template": { "name": "order_shipped", "parameters": { "name": "Sara", "tracking": "SA123456789" } }}| Field | Notes |
|---|---|
name | Template name. Or use id. |
parameters | Body variables, keyed by name, or by position ("1", "2") for positional templates. |
header_parameters | Header variables. Meta indexes these separately from body variables, so header {{1}} and body {{1}} are different values. |
button_parameters | Button values keyed by button index: the variable of a dynamic URL button, the code of a copy-code button, or the payload of a quick-reply button. See below. |
header_media | { "id": "..." } or { "link": "https://...", "filename": "..." } for templates whose header is an image, video or document. |
Every variable a template declares must be supplied. Missing ones are refused
with missing_template_parameter, naming what is missing and what the template
expects. WhatsApp itself does not check this: it delivers the message with an
empty gap in the sentence and reports success, so this is the only place it can
be caught.
GET /v1/templates lists each template’s variables.
For authentication templates with a copy-code button, passing the code as
parameter "1" fills the button automatically.
Quick-reply buttons
Section titled “Quick-reply buttons”A template with quick-reply buttons is a question. To get the answer back as data rather than text, give each button a payload and the message a reference:
{ "to": "+966500000000", "type": "template", "reference": "order-1001", "bot_reply": false, "template": { "name": "delivery_confirmation", "parameters": { "name": "Sara", "order": "1001" }, "button_parameters": { "0": "confirm", "1": "reschedule" } }}button_parameters sets the payload of each quick-reply button by its
position on the template. The contact sees the button’s text; when they tap
it, a button.reply event carries the payload as button.id and the
reference as in_reply_to.reference, so your system learns which question
was answered and how, without parsing anything. Without a payload, button.id
is the button text.
bot_reply: false keeps the chatbot out of the exchange: the tap, and
anything the contact types in reply to this message, reaches your webhooks and
the inbox but is not answered by automation. Leave it at the default when the
bot should carry the conversation on from the tap.
callback_url delivers the same event to one URL without a webhook
subscription. It is not signed, so put a token in the URL if you need to
authenticate it.
See button.reply for the payload and
React to button taps for the full flow.
{ "to": "+966500000000", "type": "document", "document": { "link": "https://example.com/invoices/1001.pdf", "filename": "Invoice-1001.pdf", "caption": "Your invoice" }}| Field | Notes |
|---|---|
link | A public URL. We fetch it and upload it to WhatsApp. Maximum 15 MB. |
id | A media id already uploaded to Meta. Use this for anything larger, or to avoid re-uploading. |
caption | Shown under the media. Not supported for audio. |
filename | Documents only. WhatsApp shows it to the recipient. |
voice | Audio only. true sends it as a push-to-talk voice note. |
Links must be public http or https. Private and internal addresses are
refused, and refused again at connection time if DNS resolves to one.
Response
Section titled “Response”{ "id": "6f1c2d3e-...", "contact_id": "3f2a...", "to": "+966500000000", "from": "Main", "type": "text", "direction": "outgoing", "status": "pending", "content": "Your order has shipped.", "has_media": false, "created_at": "2026-08-20T09:15:00Z"}pending means accepted, not delivered. The message is stored and queued;
WhatsApp has not confirmed anything. Subscribe to message.delivered,
message.read and message.failed, or poll the endpoint below.
GET /v1/messages/{id}
Section titled “GET /v1/messages/{id}”One message and its current status.
Scope: messages:read
{ "id": "6f1c2d3e-...", "contact_id": "3f2a...", "to": "+966500000000", "from": "Main", "type": "template", "direction": "outgoing", "status": "delivered", "template_name": "order_shipped", "wamid": "wamid.HBgMOTY2NTAwMDAwMDAwFQIAERgS...", "has_media": false, "created_at": "2026-08-20T09:15:00Z", "updated_at": "2026-08-20T09:15:04Z"}Statuses
Section titled “Statuses”| Status | Meaning |
|---|---|
pending | Accepted by us, not yet handed to WhatsApp |
sent | WhatsApp accepted it |
delivered | It reached the recipient’s device |
read | The recipient opened it |
failed | WhatsApp refused it. error_message says why. |
A message can go from pending straight to failed. Statuses never move
backwards, so read will not revert to delivered.
GET /v1/media/{message_id}
Section titled “GET /v1/media/{message_id}”Streams the bytes of a media message. Returns the file, not JSON, with the original content type and filename.
Scope: messages:read
curl https://k-message.kerneltics.com/v1/media/6f1c2d3e-... \ -H "Authorization: Bearer km_live_YOUR_KEY" \ -o invoice.pdf404 if the message has no media attached, or is not yours.