Messages
Send pre-approved WhatsApp template messages through the tenant's active WABA.
The Messages API sends WhatsApp messages. Only pre-approved template messages are sendable via the Public API — free-form session messaging is dashboard-only because it requires careful policy enforcement (24-hour reply window, opt-out tracking) Lynkist does not yet expose to API integrators.
| Scope | Endpoint |
|---|---|
messages:send | POST /messages/template |
Base path: /api/v1/public/messages
Prerequisites
- The tenant must have an active, connected WABA (WhatsApp Business Account). If not, the
endpoint returns
404 No active WABA account for this tenant. - The
template_namemust exist and be inapprovedstatus under that WABA. See Templates for the approval flow. - The recipient phone number must be in E.164 format: the country code, then the number,
e.g.
+919999999999. See Phone number format. - The wallet must hold enough credit for the message, else the send returns
402.
POST /messages/template — Send a template message
curl -X POST https://lynkist.io/api/v1/public/messages/template \
-H "Authorization: Bearer $LYNKIST_API_KEY" \
-H "Idempotency-Key: 7c4d2e1a-…" \
-H "Content-Type: application/json" \
-d '{
"to": "+919999999999",
"template_name": "order_confirmation",
"language": "en_US",
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Anurag" },
{ "type": "text", "text": "ORD-1042" }
]
}
]
}'Request body
| Field | Type | Required | Notes |
|---|---|---|---|
to | string | Yes | Recipient phone in E.164 (+<country><number>). See Phone number format. |
template_name | string | Yes | Approved template name. |
language | string | No | Template locale, e.g. en_US, hi. Must match the approved language exactly. Required only when the template is approved in more than one language. |
components | object[] | No | Variable substitutions. Required only if the template has variables. |
Phone number format
to is an E.164 number: the country code, then the
subscriber number, at most 15 digits.
| You send | Result |
|---|---|
+919999999999 | Accepted. |
919999999999 | Accepted. The + is optional. |
+91 99999-99999 | Accepted. Spaces, hyphens, dots and brackets are removed. |
9999999999 | 400 — no country code. Lynkist does not guess the country. |
+91abc9999999999 | 400 — letters are not allowed. |
+9184599983838383 | 400 — not a valid number for any country. |
The error is 400 VALIDATION_ERROR, and error.details names the to field. The response
returns to in E.164 with the +.
components shape
The array follows Meta's Cloud API conventions. Each entry targets one component on the
approved template (header, body, or button) and supplies parameters that fill its {{n}}
placeholders in order.
[
{
"type": "header",
"parameters": [
{ "type": "image", "image": { "link": "https://example.com/banner.jpg" } }
]
},
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Anurag" },
{ "type": "text", "text": "ORD-1042" },
{ "type": "text", "text": "https://example.com/track/ORD-1042" }
]
},
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [
{ "type": "text", "text": "ORD-1042" }
]
}
]Parameter type values: text, currency, date_time, image, document, video, payload.
See Meta's parameter reference
for the full grammar.
Response
200 OK
{
"message_id": "wamid.HBgM…",
"status": "accepted",
"to": "+919999999999"
}status is always "accepted" on success — it means the WhatsApp upstream accepted the message
for delivery. It does not mean delivered. To track real delivery, subscribe to the
message.* webhook events:
| Event | Means |
|---|---|
message.sent | Lynkist handed the message to Meta. |
message.delivered | Meta delivered to the recipient device. |
message.read | Recipient opened the message (if read receipts on). |
message.failed | Meta reported a permanent failure (carries error code). |
message_id is Meta's wamid — quote it when correlating with webhook events or Meta support.
In the dashboard inbox
A template sent through the API shows in the chat with an API label and the name of the key that sent it. It does not move the chat to the top of your team's inbox: a chat that holds only API, campaign or integration messages is listed under More → Automated, and joins the inbox when the customer replies. So a high-volume integration (order updates, OTPs) never buries the chats your team must answer.
Idempotency
Send Idempotency-Key: <uuid-v4> on every request. Retries with the same key replay the cached
response instead of sending a duplicate message — the entry lives 24 hours. See
API Reference → Idempotency.
Errors
| Status | error.code | When |
|---|---|---|
400 | VALIDATION_ERROR | A missing field, malformed JSON, a to that is not E.164, or a template approved in more than one language sent without language. |
400 | INVALID_TEMPLATE_PARAMETERS, TEMPLATE_REJECTED | The components do not match the template. |
400 | RECIPIENT_UNDELIVERABLE, MEDIA_REJECTED, MESSAGE_SEND_FAILED | WhatsApp rejected this message. |
401 | INVALID_API_KEY, API_KEY_EXPIRED | The API key is missing, wrong, revoked or expired. |
402 | INSUFFICIENT_WALLET_BALANCE | Not enough wallet credit. Top up the wallet, then retry. |
403 | INSUFFICIENT_SCOPE | The key lacks messages:send. |
403 | SENDING_NOT_ALLOWED | The workspace or its plan cannot send WhatsApp messages. |
404 | TEMPLATE_NOT_FOUND | No approved template with that name (in that language, when you sent one). |
404 | WHATSAPP_NOT_CONNECTED | No active WhatsApp account is connected. Connect one in the dashboard first. |
409 | DISPLAY_NAME_NOT_APPROVED, CUSTOMER_SERVICE_WINDOW_CLOSED, TEMPLATE_UNAVAILABLE, ACCOUNT_RESTRICTED, WHATSAPP_PAYMENT_REQUIRED, NUMBER_NOT_REGISTERED, WHATSAPP_CONNECTION_ERROR | WhatsApp refused the send, and something must be fixed first. error.message says what. |
429 | RATE_LIMIT_EXCEEDED | Your rate limit. Wait for Retry-After. |
429 | SEND_RATE_LIMITED | WhatsApp is limiting sends from this number. Wait, then retry. |
502, 503 | UPSTREAM_ERROR, WHATSAPP_UNAVAILABLE | A failure on our side or at WhatsApp. Retry with backoff. |
504 | SEND_OUTCOME_UNKNOWN | WhatsApp did not confirm in time; the message can still arrive. Do not resend automatically. |
Every send error has retryable (true or false) beside code and message.
A message.failed webhook may still arrive even on a 200 OK — that means Meta accepted but
then could not deliver (unreachable number, opted out, template paused, etc.). The webhook
payload includes Meta's numeric error code.
Common pitfalls
- Language mismatch.
languagemust match the approved language exactly. A template approved inen_USis not found underen(404) — listGET /templatesto see exact values. Leavelanguageout when the template has only one approved language. - Variable count mismatch. Sending fewer
parametersthan the template has{{n}}placeholders rejects with400. - Sending to opted-out numbers. The send returns
200 OK, then arrives asmessage.failedvia webhook. Track opt-outs in your contact custom fields. - Marketing rate limits. Meta enforces per-WABA marketing tiers separately from Lynkist's
rate limits. A
message.failedwith code131056means you hit Meta's marketing cap.