Sending messages
WhatsApp does not let a business message a customer freely. What you may send depends on whether the customer has messaged you recently.
The 24-hour service window
Section titled “The 24-hour service window”When a customer sends a message, a 24-hour window opens. Inside it a business may reply with free text. Outside it, only a template that Meta has approved in advance may be sent.
This is Meta’s rule, not ours.
Give exactly one of template or text on a send. A template works at any time; free
text works only while the window is open.
Sending a template
Section titled “Sending a template”curl -s -X POST "$TKANA_API/messages" \ -H "Authorization: Bearer $TKANA_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "to": { "phone": "+966501234567" }, "channel": "whatsapp", "template": { "name": "booking_reminder", "language": "ar", "variables": { "customer_name": "Sara", "time": "14:30" } } }'You get 202 with the provider’s message id:
{ "id": "wamid.HBgMOTY2NTAxMjM0NTY3...", "conversationId": null, "status": "sent", "sentAt": "2026-09-22T21:30:00.000Z"}An unknown phone creates the customer, so you do not have to look one up first. Give
to.customerId instead when you already know them — one or the other, never both. A send
that is refused creates nobody.
The Idempotency-Key is required here, unlike elsewhere in this API: a retried send
that went through twice is a message your customer received twice. Without one you get
422.
You cannot choose the conversation a message lands in. It joins the customer’s WhatsApp
conversation when it is delivered, so conversationId is refused rather than quietly
ignored.
Variables
Section titled “Variables”Bind every variable the template declares, by name:
- A named template (
{{customer_name}}) takes those names as keys. - A positional template (
{{1}},{{2}}) takes the numbers as keys:{"1": "Sara"}.
The names come from your template as Meta holds it, not from anything you send, so a
key we do not recognise does not silently replace one we do. A missing one is
missing_variables, and every missing name is listed in errors — Meta rejects a template
with an empty parameter rather than sending it with a gap.
A template is also per language: booking_reminder in Arabic and in English are two
separate approved templates, so name the language explicitly.
When a send is refused
Section titled “When a send is refused”| Code | Status | What to do |
|---|---|---|
channel_not_connected |
409 |
The organization has no connected WhatsApp number. Connect one in the portal under Channels. |
template_not_approved |
422 |
No template by that name and language, or Meta has not approved it yet. detail carries the status. |
missing_variables |
422 |
Bind the names listed in errors. |
provider_error |
502 |
Meta refused the send; detail carries their code and message, for example 131047 re-engagement message. |
Replying in free text
Section titled “Replying in free text”Inside the window, reply in plain text — no approved template needed:
curl -s -X POST "$TKANA_API/messages" \ -H "Authorization: Bearer $TKANA_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "to": { "customerId": "0d2f6a7e-4c3b-4b1a-9c58-2f1e8b6a9d10" }, "channel": "whatsapp", "text": "Your technician is on the way and will arrive by 4 pm." }'Unlike a template, the reply is recorded in the conversation immediately, so the
response carries a real conversationId and the message appears in the portal thread at
once, labelled with your API key’s name.
Outside the window you get 422 outside_service_window, with the moment it closed in
detail, and nothing is sent — so falling back to a template is safe.
Only the customer’s own messages open the window. Your replies do not extend it, which
is why a busy thread can still be closed to free text. If you are unsure, send the text
and fall back to a template on outside_service_window; that costs one refused call and
is more reliable than tracking the clock yourself.
Following delivery
Section titled “Following delivery”curl -s "$TKANA_API/messages/$MESSAGE_ID" \ -H "Authorization: Bearer $TKANA_KEY"status moves sent → delivered → read, or failed with the provider’s reason in
failureReason — Meta reports a failure asynchronously, minutes after accepting the send.
Poll this endpoint for a template send. The message.sent webhook fires for messages
that are in a conversation, and a template joins the conversation only once it is
delivered, so it is not the event to wait on here.
conversationId is null until the message is confirmed delivered. The customer’s
conversation shows what they actually received, so a message sits outside the thread until
Meta says it arrived — then it appears, labelled with the name of the API key that sent it.