Safravo Logosafravo.com

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

ParameterTypeDescription
categorystringFilter by MARKETING, UTILITY, or AUTHENTICATION
languagestringFilter 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 payload
resp = 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

FieldDescription
idSafravo internal template ID
meta_template_idMeta's own template ID — cross-reference with Business Manager
nameTemplate name used in POST /v1/messages/templates
parameter_formatAlways POSITIONAL — variables are {{1}}, {{2}}, etc.
categoryMARKETING, UTILITY, or AUTHENTICATION
componentsFull Meta-style component array (HEADER, BODY, FOOTER, BUTTONS)
variable_countNumber of {{n}} variables in the body — tells you exactly how many parameters to send
send_exampleReady-to-POST payload — copy directly into POST /v1/messages/templates
created_atWhen 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

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.

On this page