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. |
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 | Dynamic URL or copy-code button values, keyed by button index. |
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.
{ "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.