Skip to content
Contents
Workspace data

Templates

A template is reusable message content with named placeholders. It is defined once in the platform's own format, and stays provider-independent — so the same template survives a move from one channel to another.

Endpoints

GET/v1/templatesPOST/v1/templatesGET/v1/templates/{templateId}PATCH/v1/templates/{templateId}DELETE/v1/templates/{templateId}POST/v1/templates/{templateId}/previewPOST/v1/templates/{templateId}/test-send

Reading needs the manage scope

Every template route, including the read ones, requires templates.manage. There is no separate read scope, so a key that only lists templates still needs the managing one.

The template

A template is identified by its slug and its language together — the same slug in two languages is two templates.

FieldTypeRequiredDescription
slugstringRequiredlower_snake_case, e.g. order_confirmation. Immutable after creation.
languagestringRequiredA language tag such as ar or en-GB. Immutable after creation.
bodystringRequiredThe message text, with {{...}} placeholders.
namestringOptionalA human label. Defaults to the slug.
category"authentication" | "utility" | "marketing"OptionalDefaults to utility. Overrides the category of any send that uses this template.
headerobjectOptionalAn optional header: text, or a media placeholder. A text header requires its text.
footerstringOptionalOptional small print.
buttonsobject[]OptionalQuick replies, URL buttons and phone-number buttons.
examplesobject<string, string>OptionalSample values for the placeholders, used by the preview and by review.
status"draft" | "local_active"OptionalStart as a draft, or as local_active to make it immediately sendable.
curl
curl -X POST https://whats.azzamkh.sa/api/v1/templates \
  -H "Authorization: Bearer $WA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "order_confirmation",
    "language": "ar",
    "category": "utility",
    "body": "مرحبا {{name}}، تم تأكيد طلبك رقم {{order_id}}.",
    "footer": "شكرا لك",
    "status": "local_active",
    "examples": { "name": "سارة", "order_id": "A-10428" }
  }'

# 201 Created — "variables" is DERIVED from the content, never sent.
# {
#   "id": "tpl_...",
#   "slug": "order_confirmation",
#   "status": "local_active",
#   "version": 1,
#   "variables": [
#     { "name": "name", "example": "سارة" },
#     { "name": "order_id", "example": "A-10428" }
#   ],
#   "delivery": { "local": true, "meta": false },
#   "bindings": []
# }

Two layers of validation, and the second one is tighter

The request schema accepts a generous size; the content validator then enforces the platform's configured limits — a shorter body, fewer buttons, fewer variables. A body that passes the first check and fails the second is a 422 naming the field, so validate against the tighter numbers.

Variables

docs.pages.templates.variables.body

  • The variable list is derived from the content on every write. You never send it, and you cannot add a variable that does not appear in the text.
  • A name is lowercase, starts with a letter, and may contain digits and underscores. A malformed placeholder is rejected when you save the template, not when you send.
  • There are no filters, no conditionals and no property paths. If you need logic, do it before you call.
  • A value containing something that looks like a placeholder is inserted literally and never re-scanned.
  • Header, body, footer and button URLs are scanned for variables, in that order. Button text is not rendered into the message body.

Preview before you send

The preview endpoint renders the template and reports which variables are missing and which of yours went unused. It never fails on a missing value — a real send does, with TEMPLATE_VARIABLE_MISSING. See sending messages.

curl
curl -X POST https://whats.azzamkh.sa/api/v1/templates/tpl_.../preview \
  -H "Authorization: Bearer $WA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "variables": { "name": "سارة" } }'

# 200 OK — preview REPORTS what is missing; it does not fail.
# {
#   "id": "tpl_...",
#   "text": "مرحبا سارة، تم تأكيد طلبك رقم {{order_id}}.",
#   "variables": ["name", "order_id"],
#   "missing": ["order_id"],
#   "unused": []
# }

Status

Status controls whether a template can be sent. Three of the nine cannot.

StatusCan be sentDescription
draftNoBeing written. Not sendable.
local_activeYesLive on the platform's own providers. This is the normal working state today.
submittedYesSent for Meta review. Reserved — submission is not built.
pendingYesAwaiting Meta review. Reserved.
approvedYesApproved by Meta. Reserved.
rejectedYesRejected by Meta. Still sendable locally.
pausedNoTemporarily stopped. Not sendable.
disabledNoRetired. Not sendable. Archiving sets this.
outdatedYesThe content changed after a Meta binding was made. Still sendable locally.

Deleting a template archives it — the status becomes disabled and the messages that used it stay readable. Re-creating the same slug and language afterwards revives the archived template rather than conflicting with it.

Meta templates

Submission to Meta is not yet available

There is no submit-to-meta endpoint and no sync endpoint; a request to either is a 404, not a 501. bindings is present in every response and is always empty, and delivery.meta is therefore always false.

Everything else is built for it. A content change bumps version and writes an immutable snapshot, so a template can be matched to an approved Meta version later without losing what was actually sent.

Filters

ParameterDescription
qCase-insensitive search over slug, name and body.
statusFilter by status.
categoryFilter by category.
languageExact match on the language tag.
limit, afterCursor pagination. Note this list is ordered ascending, unlike messages and conversations.

An unknown filter value is ignored here

Unlike the message and contact lists, an unrecognised status or category on this endpoint is dropped rather than rejected: the filter simply does not apply. Check your spelling if a filter seems to do nothing.