Templates API
Discover approved WhatsApp templates and use them to send personalised messages at scale.
Templates are pre-approved WhatsApp message formats managed through Meta Business Manager. Use this API to discover which templates are available in your workspace, understand their variable structure, and copy the ready-made send_example payload directly into your POST /v1/messages/templates call.
Only templates with "status": "APPROVED" are returned. Templates sync from
Meta and may take up to 7 days after approval to appear here.
List approved templates
GET /v1/templates
Required scope: read:templates · Rate limit: 30 req/min
Query parameters
| Parameter | Type | Description |
|---|---|---|
category | string | Filter by MARKETING, UTILITY, or AUTHENTICATION |
language | string | Filter by language code, e.g. en or sw |
# All approved templates
curl "https://api.safravo.com/v1/templates" \
-H "X-API-Key: saf_live_your_key"
# Filter by category and language
curl "https://api.safravo.com/v1/templates?category=UTILITY&language=en" \
-H "X-API-Key: saf_live_your_key"const { data } = await client.get('/v1/templates', {
params: { category: 'UTILITY', language: 'en' },
});
// Find a specific template by name
const receipt = data.find(t => t.name === 'safravo_pos_receipt');
console.log(receipt.variable_count); // 3
console.log(receipt.send_example); // ready-to-POST payloadresp = client.get("/v1/templates", params={"category": "UTILITY", "language": "en"})
resp.raise_for_status()
templates = resp.json()
for t in templates:
if t["name"] == "order_confirmed_001":
print(f"Variables needed: {t['variable_count']}")
print(t["send_example"])$templates = Http::withHeaders(['X-API-Key' => env('SAFRAVO_API_KEY')])
->get('https://api.safravo.com/v1/templates', [
'category' => 'UTILITY',
'language' => 'en',
])
->json();
$template = collect($templates)->firstWhere('name', 'order_confirmed_001');
$example = $template['send_example'];req, _ := http.NewRequest("GET", "https://api.safravo.com/v1/templates", nil)
q := req.URL.Query()
q.Set("category", "UTILITY")
q.Set("language", "en")
req.URL.RawQuery = q.Encode()
req.Header.Set("X-API-Key", os.Getenv("SAFRAVO_API_KEY"))
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var templates []map[string]interface{}
json.NewDecoder(resp.Body).Decode(&templates)
fmt.Println("Count:", len(templates))uri = URI('https://api.safravo.com/v1/templates')
uri.query = URI.encode_www_form(category: 'UTILITY', language: 'en')
req = Net::HTTP::Get.new(uri)
req['X-API-Key'] = ENV['SAFRAVO_API_KEY']
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
resp = http.request(req)
templates = JSON.parse(resp.body)Response
Each template mirrors the Meta Graph API structure, with additional developer-friendly fields:
[
{
"id": "69fcc990dde5d517d5bc7ca4",
"meta_template_id": "722764924126036",
"name": "safravo_ltd_otp",
"parameter_format": "POSITIONAL",
"category": "AUTHENTICATION",
"language": "en",
"status": "APPROVED",
"components": [
{
"type": "BODY",
"text": "*{{1}}* is your verification code. For your security, do not share this code.",
"example": { "body_text": [["123456"]] }
},
{
"type": "FOOTER",
"text": "Expires in 10 minutes."
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "URL",
"text": "Copy code",
"url": "https://www.whatsapp.com/otp/code/?otp_type=COPY_CODE&code=otp{{1}}"
}
]
}
],
"variable_count": 1,
"send_example": {
"to": "+254712345678",
"template_name": "safravo_ltd_otp",
"language": "en",
"components": [
{
"type": "body",
"parameters": [{ "type": "text", "text": "123456" }]
},
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [{ "type": "text", "text": "123456" }]
}
]
},
"created_at": "2026-05-07T17:19:12.437Z"
}
]Response fields
| Field | Description |
|---|---|
id | Safravo internal template ID |
meta_template_id | Meta's own template ID — cross-reference with Business Manager |
name | Template name used in POST /v1/messages/templates |
parameter_format | Always POSITIONAL — variables are {{1}}, {{2}}, etc. |
category | MARKETING, UTILITY, or AUTHENTICATION |
components | Full Meta-style component array (HEADER, BODY, FOOTER, BUTTONS) |
variable_count | Number of {{n}} variables in the body — tells you exactly how many parameters to send |
send_example | Ready-to-POST payload — copy directly into POST /v1/messages/templates |
created_at | When the template was synced into Safravo |
Get template by ID
GET /v1/templates/:id
Required scope: read:templates
curl "https://api.safravo.com/v1/templates/69fcc990dde5d517d5bc7ca4" \
-H "X-API-Key: saf_live_your_key"const { data } = await client.get(`/v1/templates/${templateId}`);
console.log(data.send_example);resp = client.get(f"/v1/templates/{template_id}")
resp.raise_for_status()
example = resp.json()["send_example"]$template = Http::withHeaders(['X-API-Key' => env('SAFRAVO_API_KEY')])
->get("https://api.safravo.com/v1/templates/{$templateId}")
->json();
$sendExample = $template['send_example'];Using send_example to send a template
The send_example field is a fully-formed payload you can POST directly to /v1/messages/templates. Replace the placeholder values with your real data:
// 1. Fetch templates
const { data } = await client.get('/v1/templates', {
params: { category: 'UTILITY', language: 'en' },
});
const template = data.find(t => t.name === 'order_confirmed_001');
// 2. Clone send_example and fill in real values
const payload = {
...template.send_example,
to: customer.phone,
components: [
{
type: 'body',
parameters: [
{ type: 'text', text: customer.name }, // {{1}}
{ type: 'text', text: order.reference }, // {{2}}
],
},
],
};
// 3. Send
await client.post('/v1/messages/templates', payload);Component types
HEADER
Text headers have a text field. Media headers (IMAGE, VIDEO, DOCUMENT) require a link parameter at send time:
{
"type": "header",
"parameters": [{ "type": "image", "link": "https://example.com/banner.jpg" }]
}BODY
Populated with ordered positional parameters matching {{1}}, {{2}}, etc.:
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Jane Doe" },
{ "type": "text", "text": "ORD-98765" }
]
}BUTTONS
Static Quick Reply buttons without payloads do not need to be included at send time. However, Dynamic URL buttons and COPY_CODE (OTP) buttons require a payload at send time, or Meta returns a 404 Required parameter is missing error.
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [{ "type": "text", "text": "123456" }]
}Always check your send_example. If it includes a button component, you
must include it in your POST request.
Call GET /v1/templates once at app startup and cache the result for up to an
hour. Template lists change infrequently and caching reduces latency and API
calls.