Send a template message
Sends an approved template to a phone number, and opens the conversation when none exists. This is the endpoint behind a “message on WhatsApp” button in your own system.
One call does all of it. Kai finds or creates the conversation for
the number, finds or creates the contact behind it, fills the
template from its mirror, and sends. Name a teammate in assignee
to also assign the conversation to them.
You must set opt_in_confirmed to true on every call. This is
your assertion that the person agreed to receive messages from you.
Kai stores it on the message.
Send an Idempotency-Key header. When a call with the same key
already succeeded, Kai returns the stored response and does not send
again. A failed call releases the key, so a retry runs.
When the conversation is not assigned, Kai’s own reply policy stays in charge of the customer’s answer. Assign the conversation to a teammate when a person must handle what comes back.
curl --request POST \
--url https://kaisupport.com/api/v1/conversations/outbound \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"to": {
"phone": "+90 532 111 22 33"
},
"contact": {
"name": "Ayşe Yılmaz"
},
"template": {
"name": "randevu_hatirlatma",
"language": "tr",
"variables": [
"Ayşe Hanım"
]
},
"assignee": "baran@example.com",
"opt_in_confirmed": true
}
'import requests
url = "https://kaisupport.com/api/v1/conversations/outbound"
payload = {
"to": { "phone": "+90 532 111 22 33" },
"contact": { "name": "Ayşe Yılmaz" },
"template": {
"name": "randevu_hatirlatma",
"language": "tr",
"variables": ["Ayşe Hanım"]
},
"assignee": "baran@example.com",
"opt_in_confirmed": True
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
to: {phone: '+90 532 111 22 33'},
contact: {name: 'Ayşe Yılmaz'},
template: {name: 'randevu_hatirlatma', language: 'tr', variables: ['Ayşe Hanım']},
assignee: 'baran@example.com',
opt_in_confirmed: true
})
};
fetch('https://kaisupport.com/api/v1/conversations/outbound', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://kaisupport.com/api/v1/conversations/outbound",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'to' => [
'phone' => '+90 532 111 22 33'
],
'contact' => [
'name' => 'Ayşe Yılmaz'
],
'template' => [
'name' => 'randevu_hatirlatma',
'language' => 'tr',
'variables' => [
'Ayşe Hanım'
]
],
'assignee' => 'baran@example.com',
'opt_in_confirmed' => true
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://kaisupport.com/api/v1/conversations/outbound"
payload := strings.NewReader("{\n \"to\": {\n \"phone\": \"+90 532 111 22 33\"\n },\n \"contact\": {\n \"name\": \"Ayşe Yılmaz\"\n },\n \"template\": {\n \"name\": \"randevu_hatirlatma\",\n \"language\": \"tr\",\n \"variables\": [\n \"Ayşe Hanım\"\n ]\n },\n \"assignee\": \"baran@example.com\",\n \"opt_in_confirmed\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://kaisupport.com/api/v1/conversations/outbound")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"to\": {\n \"phone\": \"+90 532 111 22 33\"\n },\n \"contact\": {\n \"name\": \"Ayşe Yılmaz\"\n },\n \"template\": {\n \"name\": \"randevu_hatirlatma\",\n \"language\": \"tr\",\n \"variables\": [\n \"Ayşe Hanım\"\n ]\n },\n \"assignee\": \"baran@example.com\",\n \"opt_in_confirmed\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://kaisupport.com/api/v1/conversations/outbound")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"to\": {\n \"phone\": \"+90 532 111 22 33\"\n },\n \"contact\": {\n \"name\": \"Ayşe Yılmaz\"\n },\n \"template\": {\n \"name\": \"randevu_hatirlatma\",\n \"language\": \"tr\",\n \"variables\": [\n \"Ayşe Hanım\"\n ]\n },\n \"assignee\": \"baran@example.com\",\n \"opt_in_confirmed\": true\n}"
response = http.request(request)
puts response.read_body{
"data": {
"conversation": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"channel_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"status": "open",
"created_at": "2023-11-07T05:31:56Z",
"assigned_member_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"assigned_via": "member",
"last_message_preview": "<string>",
"last_message_at": "2023-11-07T05:31:56Z",
"contact_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"source_type": "<string>"
},
"existing_conversation": true,
"message": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"external_id": "<string>",
"text": "<string>",
"delivery_status": "sent",
"ts": "2023-11-07T05:31:56Z"
}
}
}{
"error": {
"type": "invalid_request",
"message": "Send at least one of \"external_id\", \"email\", \"phone\" or \"identities\" so the contact can be matched."
}
}{
"error": {
"type": "unauthorized",
"message": "That API key is not valid."
}
}{
"error": {
"type": "forbidden",
"message": "This key does not have the \"contacts:write\" permission."
}
}{
"error": {
"type": "not_found",
"message": "No such contact."
}
}{
"error": {
"type": "<string>",
"message": "<string>",
"details": "<unknown>"
}
}{
"error": {
"type": "<string>",
"message": "<string>",
"details": "<unknown>"
}
}{
"error": {
"type": "<string>",
"message": "<string>",
"details": "<unknown>"
}
}Authorizations
Send your API key as Authorization: Bearer kai_live_....
Make, edit and revoke keys in Kai, under Settings → API. Kai stores only a hash of the key, so the full key is shown one time. You can change the name and the permissions of a key later without a new key.
Headers
A unique string per send, at most 200 characters.
Body
Exactly one of phone or contact_id.
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Must be true. Your assertion that this person opted in to your messages.
Optional when the workspace has one WhatsApp channel.
Profile fields for a contact that this send creates.
Show child attributes
Show child attributes
A member id or a member email. The conversation is assigned to them after the send.
Response
The message was sent.
Show child attributes
Show child attributes
curl --request POST \
--url https://kaisupport.com/api/v1/conversations/outbound \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"to": {
"phone": "+90 532 111 22 33"
},
"contact": {
"name": "Ayşe Yılmaz"
},
"template": {
"name": "randevu_hatirlatma",
"language": "tr",
"variables": [
"Ayşe Hanım"
]
},
"assignee": "baran@example.com",
"opt_in_confirmed": true
}
'import requests
url = "https://kaisupport.com/api/v1/conversations/outbound"
payload = {
"to": { "phone": "+90 532 111 22 33" },
"contact": { "name": "Ayşe Yılmaz" },
"template": {
"name": "randevu_hatirlatma",
"language": "tr",
"variables": ["Ayşe Hanım"]
},
"assignee": "baran@example.com",
"opt_in_confirmed": True
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
to: {phone: '+90 532 111 22 33'},
contact: {name: 'Ayşe Yılmaz'},
template: {name: 'randevu_hatirlatma', language: 'tr', variables: ['Ayşe Hanım']},
assignee: 'baran@example.com',
opt_in_confirmed: true
})
};
fetch('https://kaisupport.com/api/v1/conversations/outbound', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://kaisupport.com/api/v1/conversations/outbound",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'to' => [
'phone' => '+90 532 111 22 33'
],
'contact' => [
'name' => 'Ayşe Yılmaz'
],
'template' => [
'name' => 'randevu_hatirlatma',
'language' => 'tr',
'variables' => [
'Ayşe Hanım'
]
],
'assignee' => 'baran@example.com',
'opt_in_confirmed' => true
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://kaisupport.com/api/v1/conversations/outbound"
payload := strings.NewReader("{\n \"to\": {\n \"phone\": \"+90 532 111 22 33\"\n },\n \"contact\": {\n \"name\": \"Ayşe Yılmaz\"\n },\n \"template\": {\n \"name\": \"randevu_hatirlatma\",\n \"language\": \"tr\",\n \"variables\": [\n \"Ayşe Hanım\"\n ]\n },\n \"assignee\": \"baran@example.com\",\n \"opt_in_confirmed\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://kaisupport.com/api/v1/conversations/outbound")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"to\": {\n \"phone\": \"+90 532 111 22 33\"\n },\n \"contact\": {\n \"name\": \"Ayşe Yılmaz\"\n },\n \"template\": {\n \"name\": \"randevu_hatirlatma\",\n \"language\": \"tr\",\n \"variables\": [\n \"Ayşe Hanım\"\n ]\n },\n \"assignee\": \"baran@example.com\",\n \"opt_in_confirmed\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://kaisupport.com/api/v1/conversations/outbound")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"to\": {\n \"phone\": \"+90 532 111 22 33\"\n },\n \"contact\": {\n \"name\": \"Ayşe Yılmaz\"\n },\n \"template\": {\n \"name\": \"randevu_hatirlatma\",\n \"language\": \"tr\",\n \"variables\": [\n \"Ayşe Hanım\"\n ]\n },\n \"assignee\": \"baran@example.com\",\n \"opt_in_confirmed\": true\n}"
response = http.request(request)
puts response.read_body{
"data": {
"conversation": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"channel_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"status": "open",
"created_at": "2023-11-07T05:31:56Z",
"assigned_member_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"assigned_via": "member",
"last_message_preview": "<string>",
"last_message_at": "2023-11-07T05:31:56Z",
"contact_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"source_type": "<string>"
},
"existing_conversation": true,
"message": {
"id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"external_id": "<string>",
"text": "<string>",
"delivery_status": "sent",
"ts": "2023-11-07T05:31:56Z"
}
}
}{
"error": {
"type": "invalid_request",
"message": "Send at least one of \"external_id\", \"email\", \"phone\" or \"identities\" so the contact can be matched."
}
}{
"error": {
"type": "unauthorized",
"message": "That API key is not valid."
}
}{
"error": {
"type": "forbidden",
"message": "This key does not have the \"contacts:write\" permission."
}
}{
"error": {
"type": "not_found",
"message": "No such contact."
}
}{
"error": {
"type": "<string>",
"message": "<string>",
"details": "<unknown>"
}
}{
"error": {
"type": "<string>",
"message": "<string>",
"details": "<unknown>"
}
}{
"error": {
"type": "<string>",
"message": "<string>",
"details": "<unknown>"
}
}