Skip to main content

Templates API

A template is a reusable message body you author once and send many times — a WhatsApp/RCS template that needs carrier approval before it can be used, an SMS or email body, or an interactive layout. Templates carry their own scope (templates:read / templates:write) distinct from messages:*, because managing a template is a content-authoring + carrier-approval action, not just a send. Base path: /api/v1/messages/templates Authentication: API key (X-API-Key) or session JWT. GET /api/v1/templates is a read-only search alias at the top level (used by the dashboard’s command-palette search) that delegates to the same list handler and scope — writes always go through /messages/templates.

Using the SDKs

The Python SDK is core-scope — it wraps the 8 core resources (messaging, voice, contacts, campaigns, verify, numbers) and reaches everything else through the generic client.request() escape hatch above. See the Python SDK. Returns the typed ApiResponse envelope. See the SDK index at SDK quickstart.

Creating a template

For channel: "whatsapp" with a components array (the rich HEADER / BODY / FOOTER / BUTTONS shape), the template is submitted for carrier approval using your own WhatsApp Business Account credentials rather than platform-shared ones. Other channels (SMS, email, RCS, etc.) use a plain content string.
A newly-created WhatsApp/RCS template starts in a pending carrier-approval status; poll GET /{id} or listen for the template-approval webhook to know when it’s ready to send. The response carries the template id and its initial status:
A WhatsApp template can only be sent once it reaches approved. For non-carrier-approval channels (SMS, email) the template is usable immediately.

Listing, updating, and deleting templates

Filter the list with ?search=, ?channel=, ?status=, or ?waba_id= — the same filter set powers the dashboard’s template picker. Read one template by id; patch the fields you want to rewrite (a WhatsApp body edit re-enters carrier approval); delete a template you no longer send.

List templates (filtered)

Get a template by id

Update a template (PATCH)

Delete a template

Sending a template message

Reference the approved template by name on the unified send endpoint and pass the positional variable substitutions in variables (keyed by the {{1}}, {{2}} placeholders you authored):
Either body, template_name, or media_url is required on every send — a template send substitutes for the freeform body. See the Messaging API reference for the full request shape. The template send returns a 202 acceptance: the message is queued and its status settles asynchronously via status webhooks.

AI localization

POST /{id}/localize auto-translates one canonical template’s body and button labels into up to 25 target locales, then creates one (channel, name, language) row per locale through the same carrier-submission path a hand-authored variant uses — so each translated variant goes through carrier approval exactly like an original. GET /{id}/localizations returns the per-locale approval tracker: every language variant sharing the template’s (channel, name), each with its own status.
The 201 response lists one entry per requested locale with a created / skipped / failed verdict, the new variant’s id for each created row, and missingLocales for locales the model returned no variant for:
A skipped entry carrying reason: "variant_already_exists" means that locale already had a variant — localization is idempotent and re-running it does not duplicate the row. Because localization creates each variant through the same create path as a hand-authored one, WhatsApp/RCS variants start pending carrier approval and platform-only channels (SMS, email) start active. GET /{id}/localizations returns the per-locale approval tracker — one row per (channel, name, language) variant sharing the source template’s identity:
Every variant sharing (channel, name) appears — whether it came from AI localization or was authored by hand. A submission_error on a variant means the carrier rejected that locale; the value is the rejection reason the carrier returned for it.

Reusable content templates

/api/v1/messages/content-templates is a separate, channel-agnostic layer above per-channel templates — group several channel-specific variants under one logical content id with an author-declared fallback chain (send WhatsApp if approved, else fall back to SMS), similar to Twilio’s Content API. A create carries the logical id, a default_channel, an ordered fallback_chain, a variants object keyed by channel, and the {{N}} placeholders it expects. For a WhatsApp variant the components array uses the same rich HEADER / BODY / FOOTER / BUTTONS shape shown in “Creating a template” above:
The 201 response returns the row as stored:
The render endpoint resolves a channel against the variant set — Target channel is optional and defaults to the template’s default_channel; params substitutes {{N}} positionally — so you can preview exactly what a recipient on the requested channel would see before you send:
When the requested channel has no variant but a fallback resolves one, rendered_channel names the fallback hop and resolution reflects that — for example, asking for sms on a WhatsApp-only content template returns "rendered_channel": "whatsapp", "resolution": "fallback". When neither the target nor any fallback resolves, the request returns 422 NO_VARIANT_FOR_CHANNEL.

Cross-campaign analytics

GET /api/v1/templates/{id}/analytics returns the consolidated engagement rollup for a template reused across many campaigns — sent / delivered / read / click counts aggregated across every campaign that used it, rather than the per-campaign view alone.
The response contains four groups of fields:
  • template — the template’s id, name, channel, and current approval status. A 404 here means the id does not exist in your tenant.
  • totals — engagement counters aggregated across every campaign that referenced the template: sent, delivered, failed, opened, clicked.
  • rates — the derived funnel: delivery_rate (delivered over terminal messages), open_rate (opened over delivered), and click_rate (clicked over delivered). Rates are fractions rounded to two decimals, e.g. 0.42 for 42%.
  • campaigns — the per-campaign breakdown, one entry per campaign that used the template with its own counters and status, so a strong- or weak-performing campaign is visible against the rollup.
A degraded: true flag on the response means the live per-message scan timed out and the numbers were read from denormalized campaign counters instead — the totals remain correct for campaigns that have run, but very recent sends may not be reflected yet. The full response envelope, with every group populated:

See also