Templates
Reusable messages with per-recipient variables, kept in sync over the API.
Templates
A template is a reusable message with variables that fill themselves in per
recipient: Salaam {name}! Your order is ready. becomes a personal message for
each contact at send time. Templates written here are the same ones your team
sees in the dashboard's library — an integration can keep them in sync with an
external CMS, and a campaign can send one to a whole audience (see
Campaigns).
Scopes: templates:read for reading and previewing, templates:write for
creating, editing and deleting.
Variables
Write a variable as a name in single curly braces: {name}. Available
everywhere: {name}, {first_name}, {phone}. Beyond those, every custom
field you have defined on contacts is available under its key, and a contact's
metadata keys work the same way. Names are case-insensitive.
A variable the contact cannot fill is left as written, not blanked — a
visible {name} in a message is caught by whoever previews it; an invisible
gap is not. The preview endpoint lists unfilled variables so you can catch them
before sending.
Create a template
POST /api/v1/templates
{
"name": "Order ready",
"content": "Salaam {name}! Your order is ready for pickup.",
"category": "notifications",
"shortcut": "/ready",
"media_id": null
}
| Field | Required | Notes |
|---|---|---|
name |
yes | Up to 160 characters |
content |
yes | The message body, up to 4,096 characters, variables in {braces} |
category |
no | Free-form label for organising the library, up to 60 characters |
shortcut |
no | A /slash shortcut your team can type in the inbox composer |
media_id |
no | Id of a retained media object to attach (see Media) |
Returns 201 with the template. id is the identifier every other call uses.
The response also lists the variables found in the body, so you can validate
what a sync just uploaded:
{
"data": {
"id": "9b2f…",
"name": "Order ready",
"content": "Salaam {name}! Your order is ready for pickup.",
"category": "notifications",
"shortcut": "/ready",
"variables": ["name"],
"attachment": null,
"usage_count": 0,
"last_used_at": null
}
}
Send an Idempotency-Key header on creation, as with every write (see
Errors and idempotency).
List and read
GET /api/v1/templates
GET /api/v1/templates?search=order
GET /api/v1/templates/{id}
The list is paginated (page, per_page up to 100) and search matches the
name. Each item carries usage_count and last_used_at, so a sync can tell
which templates are actually earning their place.
Preview — what will this contact actually receive?
GET /api/v1/templates/{id}/preview
GET /api/v1/templates/{id}/preview?contact_id={contact_id}
Renders the stored body with a real contact's values — the named one, or the workspace's first contact if you name none. This uses the same renderer a real send uses, so what the preview shows is what dispatch produces:
{
"data": {
"contact": { "id": "d1c4…", "name": "Ayesha Khan", "phone": "+923001234567" },
"body": "Salaam Ayesha Khan! Your order is ready for pickup.",
"missing": []
}
}
missing names every variable that contact could not fill. A non-empty
missing on your typical contacts means the message will go out with literal
braces in it — fix the template or the data before a campaign does it at scale.
Update and delete
PUT /api/v1/templates/{id}
DELETE /api/v1/templates/{id}
PUT takes the same fields as creation; omitted fields keep their value.
DELETE returns 204.
Deleting a template does not touch campaigns that already used it: a campaign resolves the template to concrete words at the moment it is scheduled, not at dispatch, precisely so a later edit or deletion cannot change what an approved campaign says.