> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kaisupport.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Send template messages

> Send an approved WhatsApp template from your own system, and open the conversation when none exists.

This guide adds a "Send on WhatsApp" button to your CRM or admin panel. The
button sends an approved template to a phone number. When the number has no
conversation in Kai, the send opens one.

WhatsApp accepts free text only in the 24 hours after the customer's last
message. Outside this window, and for a first contact, WhatsApp accepts only
a template that Meta approved. This is a rule of Meta, not of Kai.

The key for this guide needs the `messages:send` permission. This permission
includes `templates:read` and `conversations:read`.

## 1. Find the channel

Read the channel list once and store the id of your WhatsApp channel:

```bash theme={null}
curl https://kaisupport.com/api/v1/channels \
  -H "Authorization: Bearer $KAI_API_KEY"
```

```json theme={null}
{
  "data": [
    {
      "id": "7c1f4a80-...",
      "kind": "whatsapp_cloud",
      "external_id": "104857923341",
      "enabled": true,
      "supports_templates": true,
      "created_at": "2026-05-11T09:00:00.000Z"
    }
  ]
}
```

<Note>
  When the workspace has one WhatsApp channel, you can omit `channel_id` in
  the next steps. Kai finds the channel for you.
</Note>

## 2. List the templates, with hints

Ask for the templates when your user opens the send window. Add the email
address of the teammate that sends the message, and the customer:

```bash theme={null}
curl -G "https://kaisupport.com/api/v1/templates" \
  -H "Authorization: Bearer $KAI_API_KEY" \
  --data-urlencode "agent=baran@example.com" \
  --data-urlencode "phone=+905356328781" \
  --data-urlencode "contact_name=Gülsüm Bodur"
```

```json theme={null}
{
  "channel_id": "7c1f4a80-...",
  "agent": { "member_id": "2c9a0d51-...", "name": "Baran Demir", "email": "baran@example.com" },
  "contact": { "contact_id": "4f1d6f2a-...", "name": "Gülsüm Bodur" },
  "data": [
    {
      "name": "consultation_request_cant_be_reached_reschedule_tr",
      "language": "tr",
      "status": "approved",
      "header": "Derece Kampüsü · {{konu}}",
      "body": "Selamlar {{customer_name}} ben Derece Kampüsü'nden {{consultant_name}}, ön görüşme talep etmişsin ancak sana ulaşamadım.",
      "variables": ["header.konu", "customer_name", "consultant_name"],
      "variable_hints": [
        { "address": "header.konu", "key": "konu", "part": "header", "binding": "free", "value": null, "source": null },
        { "address": "customer_name", "key": "customer_name", "part": "body", "binding": "customer_name", "value": "Gülsüm Bodur", "source": "request" },
        { "address": "consultant_name", "key": "consultant_name", "part": "body", "binding": "agent_name", "value": "Baran Demir", "source": "member" }
      ],
      "sendable": true
    }
  ]
}
```

The response holds only the templates that you can send. `variables` names
the placeholders, in the order that your send must fill them.

### Templates with a title

A template can have a title above the message. WhatsApp shows the title in
bold on the first line. `header` holds the title, or `null` when the
template has none.

The title can have its own variables, and Kai fills them. WhatsApp numbers
and names the variables of the title and of the message separately, so
`{{1}}` in the title and `{{1}}` in the message are two different values.

Each variable therefore has an **address**, which is the name you use when
you send:

| Where | Placeholder | Address |
| - | - | - |
| Message | `{{customer_name}}` | `customer_name` |
| Title | `{{konu}}` | `header.konu` |

The `variables` field lists these addresses in the order that a send fills
them: the variables of the title first, then the variables of the message.
Each hint also holds `part`, which is `header` or `body`, so you can group
your inputs.

<Note>
  Kai cannot send a template with an image, a video or a document title,
  because that title needs a file and not a value. Kai also cannot send a
  template with a variable link in a button. These templates are absent from
  the response. Add `status=all` to see them, where `sendable` is `false`.
</Note>

### Use the hints

Each entry in `variable_hints` tells you how to fill one input:

* A `value` that is not `null` is the value that Kai would use. Prefill your
  input with it. Your user can change it before they send.
* A `value` of `null` means that Kai cannot fill this variable. Ask your user
  for it, and do not send until they answer.

`binding` says where the value comes from. `agent_name` reads the profile of
the teammate in `agent`. `customer_name` reads the contact. `free` means
that a person types it every time. The workspace configures a binding under
**Settings → Channels**. Kai guesses one from the name of the variable when
nobody configured it, so `{{consultant_name}}` and `{{customer_name}}` work
without setup.

An `agent` address that no member holds returns `"agent": null` and empty
hints for the agent variables. Read this field when you debug an empty
input.

<Note>
  Kai never fills a variable at send time. The send needs every value. Your
  form is what the person reads before the message leaves, so a value that
  Kai adds after that screen is a value that nobody approved.
</Note>

Kai mirrors the template library of the channel. To add or change a
template, use the WhatsApp Manager of Meta, then sync in Kai under
**Settings → Channels**.

## 3. Send the template

```bash theme={null}
curl -X POST https://kaisupport.com/api/v1/conversations/outbound \
  -H "Authorization: Bearer $KAI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-reachout-8842" \
  -d '{
    "to": { "phone": "+905356328781" },
    "contact": { "name": "Gülsüm Bodur" },
    "template": {
      "name": "consultation_request_cant_be_reached_reschedule_tr",
      "language": "tr",
      "variables": {
        "header.konu": "Ön görüşme",
        "customer_name": "Gülsüm Bodur",
        "consultant_name": "Baran Demir"
      }
    },
    "assignee": "baran@example.com",
    "opt_in_confirmed": true
  }'
```

Send the values from your form, which hold the hints or the changes that
your user made. Every variable is required.

### Two shapes for the values

A template that names its variables accepts an object, as above. The keys
are the addresses from step 2. Kai puts the values in the order of the
template, so you do not need to know that order.

A template that numbers all of its variables (`{{1}}`, `{{2}}`) accepts an
array, in the order of the `variables` field from step 2. The values of the
title come first:

```json theme={null}
"variables": ["Ön görüşme", "Gülsüm Bodur", "Baran Demir"]
```

An array also works for a named template. But then your values follow the
order of the template, and somebody can change that order in the WhatsApp
Manager of Meta. Two values that change places are still two strings, so
nothing fails and the customer reads the wrong name. Use the object for a
named template.

Kai refuses a variable name that the template does not hold, and names it in
the error. A misspelled name is a fault in your integration, not a value to
drop. A plain name is always a variable of the message: to fill a variable
of the title, you must write `header.` in front of it.

One call does all of it. Kai finds or makes the conversation for the number.
Kai finds or makes the contact behind it. Kai fills the title and the body
of the template from its mirror and sends. Then Kai assigns the conversation to the `assignee`.

You must set `opt_in_confirmed` to `true` on every call. This is your
assertion that this person agreed to receive your messages. Kai stores the
assertion on the message.

```json theme={null}
{
  "data": {
    "conversation": {
      "id": "9d2b1c44-...",
      "status": "human",
      "assigned_member_id": "2c9a0d51-...",
      "assigned_via": "api",
      "contact_id": "4f1d6f2a-..."
    },
    "existing_conversation": false,
    "message": {
      "external_id": "wamid.HBgL...",
      "text": "Derece Kampüsü · Ön görüşme\n\nSelamlar Gülsüm Bodur ben Derece Kampüsü'nden Baran Demir, ön görüşme talep etmişsin ancak sana ulaşamadım.",
      "delivery_status": "sent",
      "ts": "2026-08-11T11:02:07.000Z"
    }
  }
}
```

`existing_conversation` is `true` when the thread already existed. Then the
message joined that thread, and no second thread exists.

You can also name the person with `"to": { "contact_id": "..." }`. Kai then
uses the phone number that the contact holds. A contact without a phone
number returns a `no_phone` error.

## 4. Retry safely

A send costs money and reaches a person. Send an `Idempotency-Key` header
with a unique value per intended message.

* When a call with the same key already succeeded, Kai returns the stored
  response and does not send again.
* When the first call failed, Kai releases the key. Your retry runs.
* When the first call still runs, Kai returns a 409 `idempotency_in_flight`
  error. Retry after a moment.
* The same key with a different body returns a 422 `idempotency_mismatch`
  error. Use a new key for a new message.

## 5. Decide who answers the reply

The send itself does not decide who handles the conversation. The `assignee`
field does.

* With `assignee`, the conversation belongs to that teammate. Kai does not
  answer on an assigned conversation. The reply of the customer waits in the
  inbox of that teammate, and in their Element X rooms.
* Without `assignee`, the conversation stays with the reply policy of the
  channel. When the channel runs on autopilot, Kai can answer the customer.

Name an `assignee` when a person must handle what comes back, for example a
sales call. Omit it when Kai can handle the reply, for example a payment
reminder.

## Errors

| `error.type` | Meaning |
| - | - |
| `template_missing` | The channel does not hold this template. Sync and retry. |
| `template_unapproved` | Meta did not approve this template, or removed the approval. |
| `variables_missing` | A variable is empty or absent. Every variable is required, including the ones with a hint. |
| `variables_unknown` | The values name a variable that this template does not hold. A variable of the title needs the `header.` prefix. |
| `variables_not_named` | The values are an object, but this template numbers its variables. Send an array. |
| `no_phone` | The contact in `to.contact_id` has no phone number. |
| `identity_conflict` | The number belongs to a conversation with a different contact. |
| `recipient_undeliverable` | WhatsApp could not deliver the last message to this number in the last 7 days. Kai does not send again until then, or until the customer writes. Call the customer instead. |
| `send_failed` | WhatsApp refused the send. The message holds the reason from Meta. |

For the error format and the general codes, see
[Error handling](/guides/errors).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.