curl --request POST \
--url https://api.ciarem.ai/v1/templates/{template_id}/send \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"recipients": {
"contact_id": "<string>",
"phone": "<string>",
"variables": {},
"coupon_code": "<string>",
"url_suffixes": {},
"lto_expiration": "2023-11-07T05:31:56Z",
"catalog_records": {},
"header_media_url": "<string>",
"card_media_urls": {}
},
"channel_id": "<string>"
}
'import requests
url = "https://api.ciarem.ai/v1/templates/{template_id}/send"
payload = {
"recipients": {
"contact_id": "<string>",
"phone": "<string>",
"variables": {},
"coupon_code": "<string>",
"url_suffixes": {},
"lto_expiration": "2023-11-07T05:31:56Z",
"catalog_records": {},
"header_media_url": "<string>",
"card_media_urls": {}
},
"channel_id": "<string>"
}
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({
recipients: {
contact_id: '<string>',
phone: '<string>',
variables: {},
coupon_code: '<string>',
url_suffixes: {},
lto_expiration: '2023-11-07T05:31:56Z',
catalog_records: {},
header_media_url: '<string>',
card_media_urls: {}
},
channel_id: '<string>'
})
};
fetch('https://api.ciarem.ai/v1/templates/{template_id}/send', 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://api.ciarem.ai/v1/templates/{template_id}/send",
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([
'recipients' => [
'contact_id' => '<string>',
'phone' => '<string>',
'variables' => [
],
'coupon_code' => '<string>',
'url_suffixes' => [
],
'lto_expiration' => '2023-11-07T05:31:56Z',
'catalog_records' => [
],
'header_media_url' => '<string>',
'card_media_urls' => [
]
],
'channel_id' => '<string>'
]),
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://api.ciarem.ai/v1/templates/{template_id}/send"
payload := strings.NewReader("{\n \"recipients\": {\n \"contact_id\": \"<string>\",\n \"phone\": \"<string>\",\n \"variables\": {},\n \"coupon_code\": \"<string>\",\n \"url_suffixes\": {},\n \"lto_expiration\": \"2023-11-07T05:31:56Z\",\n \"catalog_records\": {},\n \"header_media_url\": \"<string>\",\n \"card_media_urls\": {}\n },\n \"channel_id\": \"<string>\"\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://api.ciarem.ai/v1/templates/{template_id}/send")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"recipients\": {\n \"contact_id\": \"<string>\",\n \"phone\": \"<string>\",\n \"variables\": {},\n \"coupon_code\": \"<string>\",\n \"url_suffixes\": {},\n \"lto_expiration\": \"2023-11-07T05:31:56Z\",\n \"catalog_records\": {},\n \"header_media_url\": \"<string>\",\n \"card_media_urls\": {}\n },\n \"channel_id\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.ciarem.ai/v1/templates/{template_id}/send")
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 \"recipients\": {\n \"contact_id\": \"<string>\",\n \"phone\": \"<string>\",\n \"variables\": {},\n \"coupon_code\": \"<string>\",\n \"url_suffixes\": {},\n \"lto_expiration\": \"2023-11-07T05:31:56Z\",\n \"catalog_records\": {},\n \"header_media_url\": \"<string>\",\n \"card_media_urls\": {}\n },\n \"channel_id\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"template_id": "<string>",
"template_name": "<string>",
"template_language": "<string>",
"channel_id": "<string>",
"status": "in_progress",
"requested": 123,
"sent": 123,
"failed": 123,
"pending": 123,
"origin": "<string>",
"created_at": "2023-11-07T05:31:56Z"
}{
"detail": "<string>"
}{
"detail": "<string>"
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"input": "<unknown>",
"ctx": {}
}
]
}{
"detail": "<string>"
}Send a template
Queue an APPROVED template to 1-100 recipients.
202 acknowledges acceptance, not delivery — the returned broadcast’s
counts all start at zero and move as the worker fans the batch out. Poll
GET /v1/broadcasts/{id} for progress and
/v1/broadcasts/{id}/recipients for per-recipient outcomes.
Address each recipient by contact_id or phone. A phone that matches
no contact creates one, in the org’s initial funnel stage — sending to a
hundred unknown numbers grows the CRM by up to a hundred contacts. The same
person named twice (once by id, once by phone) is queued and charged once.
GET /v1/templates/{id}/send-requirements lists exactly what this template
needs filled. Everything it names is validated here before the wallet is
touched or Meta is called, so a refusal costs nothing: 400 an unusable or
ambiguous channel_id, 402 a batch the WhatsApp balance can’t cover, 404 an
unknown template or recipient, 409 a template that is not APPROVED or a
batch past what is left of Meta’s rolling 24h messaging window (the whole
request is refused rather than silently truncated), 422 anything missing,
unknown, or unfetchable in the parameters.
Pass Idempotency-Key to make a retry safe: the same key returns the
original broadcast instead of queuing a second one, for 24h.
curl --request POST \
--url https://api.ciarem.ai/v1/templates/{template_id}/send \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"recipients": {
"contact_id": "<string>",
"phone": "<string>",
"variables": {},
"coupon_code": "<string>",
"url_suffixes": {},
"lto_expiration": "2023-11-07T05:31:56Z",
"catalog_records": {},
"header_media_url": "<string>",
"card_media_urls": {}
},
"channel_id": "<string>"
}
'import requests
url = "https://api.ciarem.ai/v1/templates/{template_id}/send"
payload = {
"recipients": {
"contact_id": "<string>",
"phone": "<string>",
"variables": {},
"coupon_code": "<string>",
"url_suffixes": {},
"lto_expiration": "2023-11-07T05:31:56Z",
"catalog_records": {},
"header_media_url": "<string>",
"card_media_urls": {}
},
"channel_id": "<string>"
}
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({
recipients: {
contact_id: '<string>',
phone: '<string>',
variables: {},
coupon_code: '<string>',
url_suffixes: {},
lto_expiration: '2023-11-07T05:31:56Z',
catalog_records: {},
header_media_url: '<string>',
card_media_urls: {}
},
channel_id: '<string>'
})
};
fetch('https://api.ciarem.ai/v1/templates/{template_id}/send', 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://api.ciarem.ai/v1/templates/{template_id}/send",
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([
'recipients' => [
'contact_id' => '<string>',
'phone' => '<string>',
'variables' => [
],
'coupon_code' => '<string>',
'url_suffixes' => [
],
'lto_expiration' => '2023-11-07T05:31:56Z',
'catalog_records' => [
],
'header_media_url' => '<string>',
'card_media_urls' => [
]
],
'channel_id' => '<string>'
]),
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://api.ciarem.ai/v1/templates/{template_id}/send"
payload := strings.NewReader("{\n \"recipients\": {\n \"contact_id\": \"<string>\",\n \"phone\": \"<string>\",\n \"variables\": {},\n \"coupon_code\": \"<string>\",\n \"url_suffixes\": {},\n \"lto_expiration\": \"2023-11-07T05:31:56Z\",\n \"catalog_records\": {},\n \"header_media_url\": \"<string>\",\n \"card_media_urls\": {}\n },\n \"channel_id\": \"<string>\"\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://api.ciarem.ai/v1/templates/{template_id}/send")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"recipients\": {\n \"contact_id\": \"<string>\",\n \"phone\": \"<string>\",\n \"variables\": {},\n \"coupon_code\": \"<string>\",\n \"url_suffixes\": {},\n \"lto_expiration\": \"2023-11-07T05:31:56Z\",\n \"catalog_records\": {},\n \"header_media_url\": \"<string>\",\n \"card_media_urls\": {}\n },\n \"channel_id\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.ciarem.ai/v1/templates/{template_id}/send")
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 \"recipients\": {\n \"contact_id\": \"<string>\",\n \"phone\": \"<string>\",\n \"variables\": {},\n \"coupon_code\": \"<string>\",\n \"url_suffixes\": {},\n \"lto_expiration\": \"2023-11-07T05:31:56Z\",\n \"catalog_records\": {},\n \"header_media_url\": \"<string>\",\n \"card_media_urls\": {}\n },\n \"channel_id\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"template_id": "<string>",
"template_name": "<string>",
"template_language": "<string>",
"channel_id": "<string>",
"status": "in_progress",
"requested": 123,
"sent": 123,
"failed": 123,
"pending": 123,
"origin": "<string>",
"created_at": "2023-11-07T05:31:56Z"
}{
"detail": "<string>"
}{
"detail": "<string>"
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"input": "<unknown>",
"ctx": {}
}
]
}{
"detail": "<string>"
}Authorizations
An org API key: Authorization: Bearer oak_….
Headers
Path Parameters
Body
Send an APPROVED template to one recipient or a list of them.
recipients accepts a single object or an array with identical semantics —
sending to one person should not require wrapping it — and the response is
a PublicBroadcast either way.
One person to send to, plus what this template needs filled for them.
Exactly one of contact_id / phone. A phone that matches no contact
creates one (see the endpoint docstring) — the honest consequence of
addressing a CRM-backed product by phone number.
variables is keyed by the authored token — "1" for a positional
template, "discount_code" or "contact.first_name" for a named one.
Positional-vs-named is a property of the template and mixing the two is
fatal at Meta, so this is deliberately not an ordered array.
GET /v1/templates/{id}/send-requirements lists the exact keys.
- PublicSendRecipient · object
- PublicSendRecipient · object[]
Show child attributes
Show child attributes
Response
Successful Response
A queued template send. Counts are derived from the messages it produced,
so they move as the worker fans the batch out; they always sum to
requested.
Derived campaign status shown in the UI. stopped_low_balance and
cancelled mirror the stored state; the other two are computed from the
counts (no stored value — chunked async means no single worker knows it sent
the last recipient).
in_progress, completed, stopped_low_balance, cancelled