Templates
Create, validate, preview, synchronize, inspect, update, and delete WhatsApp templates.
WhatsApp templates are required for first-contact messages and messages outside the 24-hour customer-service window.
Lifecycle
Creating a WhatsApp template submits it to Meta immediately. There is no separate submit endpoint. Synchronization only refreshes the stored provider projection.
Authentication
All template operations require:
Authorization: Bearer ntfy_your_api_key
Content-Type: application/jsonOmit Content-Type for requests without a body.
Create Template
POST /v1/waba-api/templates
Prerequisite: the Portal reports the WABA connection as ready.
curl --request POST \
"$NOTIFY_API_BASE_URL/v1/waba-api/templates" \
--header "Authorization: Bearer $NOTIFY_WABA_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"channel": "whatsapp",
"name": "Order Update",
"description": "Order status notification",
"externalTemplateName": "order_update",
"externalTemplateLanguage": "en",
"externalTemplateCategory": "UTILITY",
"parameterFormat": "positional",
"componentSchema": [
{
"type": "body",
"text": "Hello {{1}}, your order {{2}} is ready.",
"variables": ["1", "2"]
}
],
"variableSchema": [
{ "key": "1", "required": true, "source": "static", "component": "body" },
{ "key": "2", "required": true, "source": "static", "component": "body" }
]
}'Body
| Field | Required | Validation |
|---|---|---|
channel | yes | Send whatsapp |
name | yes | String, 2-150 characters |
description | no | String |
externalTemplateName | yes | Lowercase letters, digits, underscores; maximum 512 characters |
externalTemplateLanguage | yes | Meta locale in xx or xx_YY form |
externalTemplateCategory | yes | MARKETING, UTILITY, or AUTHENTICATION |
parameterFormat | no | named or positional |
componentSchema | yes | Non-empty component array |
variableSchema | no | Array of variable declarations |
Success
HTTP 201 with the standard success envelope:
{
"status": 201,
"message": "Success",
"timestamp": "2026-09-09T10:10:00.000Z",
"path": "/v1/waba-api/templates",
"data": {
"id": "b3f1c2a0-1234-4a5b-9c6d-7e8f9a0b1c2d",
"channel": "whatsapp",
"sourceOfTruth": "waba",
"status": "pending",
"isActive": true,
"name": "Order Update",
"description": "Order status notification",
"variableSchema": [
{ "key": "1", "required": true, "source": "static", "component": "body" },
{ "key": "2", "required": true, "source": "static", "component": "body" }
],
"usageCount": 0,
"whatsappConfig": {
"wabaId": "9876543210",
"externalTemplateId": null,
"externalTemplateName": "order_update",
"externalTemplateLanguage": "en",
"externalTemplateCategory": "UTILITY",
"parameterFormat": "positional",
"componentSchema": [
{
"type": "body",
"text": "Hello {{1}}, your order {{2}} is ready.",
"variables": ["1", "2"]
}
],
"providerStatus": null,
"statusInfo": null
},
"sync": {
"syncStatus": null,
"lastSyncedAt": null,
"lastSyncError": null
},
"createdAt": "2026-09-09T10:10:00.000Z",
"updatedAt": "2026-09-09T10:10:00.000Z"
}
}Common failures: invalid schema (400), duplicate external name/language (409), WABA not ready (422), provider rejection or unavailable dependency.
Next: use the returned data.id as templateId, then read status.
Components
| Component | Fields |
|---|---|
header | format, text, sampleMediaServiceId, variables |
body | text, variables, addSecurityRecommendation |
footer | text, codeExpirationMinutes |
buttons | buttons |
Text and placeholders
- Body maximum: 1024 characters.
- Header/footer maximum: 60 characters.
- Header text must be one line and contain at most one placeholder.
- Footer text cannot contain placeholders.
- Positional placeholders must be contiguous from
{{1}}. - Named placeholders use identifier-style names such as
{{customer_name}}. - A placeholder cannot start or end component text.
- Consecutive placeholders require text between them.
- Do not place
*,_, or~directly beside a placeholder.
Media headers
Header format accepts text, image, video, or document. Media headers require sampleMediaServiceId containing a durable Notify Africa Media Service UUID.
Buttons
| Type | Rules |
|---|---|
quick_reply | Non-empty unique label; maximum 25 characters; maximum 3 buttons |
url | Non-empty unique label; HTTPS value; at most one {{1}} placeholder |
phone_number | Non-empty unique label; optional + then 6-15 digits |
otp | Authentication templates only; copy_code or one_tap |
- Do not mix quick replies with URL or phone buttons.
- Use at most two combined URL/phone buttons.
- When using two CTA buttons, use one URL and one phone button.
- One-tap OTP requires
packageNameandsignatureHash. - Authentication templates contain no header, use Meta-managed body/footer text, and contain exactly one OTP button.
List Templates
GET /v1/waba-api/templates
Always filter WABA documentation queries with channel=whatsapp.
curl --request GET \
"$NOTIFY_API_BASE_URL/v1/waba-api/templates?channel=whatsapp&status=approved¤tPage=1&pageSize=10&sortOrder=desc" \
--header "Authorization: Bearer $NOTIFY_WABA_API_KEY"Query Parameters
| Parameter | Required | Rule |
|---|---|---|
channel | recommended | Use whatsapp |
status | no | pending, approved, or rejected for WhatsApp |
sourceOfTruth | no | Use waba for provider-backed templates |
q | no | Search string |
currentPage | no | Positive number; default 1 |
pageSize | no | Positive number; default 10; capped at 25 |
sortBy | no | Sort field |
sortOrder | no | asc or desc |
Success
HTTP 200:
{
"status": 200,
"message": "Success",
"timestamp": "2026-09-09T10:12:00.000Z",
"path": "/v1/waba-api/templates?channel=whatsapp¤tPage=1&pageSize=10",
"data": [
{
"id": "b3f1c2a0-1234-4a5b-9c6d-7e8f9a0b1c2d",
"channel": "whatsapp",
"sourceOfTruth": "waba",
"status": "approved",
"isActive": true,
"name": "Order Update"
}
],
"pagination": {
"total": 1,
"currentPage": 1,
"totalPages": 1,
"pageSize": 10
}
}Common failures: invalid filter or sort value (400), invalid API key (401).
Next: retrieve a selected template or send one whose status is approved.
Get Template
GET /v1/waba-api/templates/:templateId
Path parameter: templateId, the Notify Africa UUID returned by create/list.
curl --request GET \
"$NOTIFY_API_BASE_URL/v1/waba-api/templates/b3f1c2a0-1234-4a5b-9c6d-7e8f9a0b1c2d" \
--header "Authorization: Bearer $NOTIFY_WABA_API_KEY"Success: HTTP 200; data contains the complete template response shown under Create Template.
Common failures: malformed UUID (400), template absent or owned by another tenant (404).
Next: validate bindings, preview, synchronize, update, delete, or send the approved external template name.
Validate Bindings
POST /v1/waba-api/templates/:templateId/validate
This checks the supplied keys against the stored variableSchema. It does not submit the template or rerun all Meta component rules.
curl --request POST \
"$NOTIFY_API_BASE_URL/v1/waba-api/templates/b3f1c2a0-1234-4a5b-9c6d-7e8f9a0b1c2d/validate" \
--header "Authorization: Bearer $NOTIFY_WABA_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"bindings": {
"1": "Jane",
"2": "ORD-1001"
}
}'Body: optional bindings object keyed exactly like variableSchema[].key.
HTTP 201:
{
"status": 201,
"message": "Success",
"timestamp": "2026-09-09T10:15:00.000Z",
"path": "/v1/waba-api/templates/b3f1c2a0-1234-4a5b-9c6d-7e8f9a0b1c2d/validate",
"data": {
"valid": true,
"errors": []
}
}Missing required static bindings and unknown keys return valid: false inside a successful response. Contact-derived variables are not required in this check.
Next: preview the same bindings.
Preview Template
POST /v1/waba-api/templates/:templateId/preview
The preview replaces matching placeholders locally. It is not a provider-rendered preview.
curl --request POST \
"$NOTIFY_API_BASE_URL/v1/waba-api/templates/b3f1c2a0-1234-4a5b-9c6d-7e8f9a0b1c2d/preview" \
--header "Authorization: Bearer $NOTIFY_WABA_API_KEY" \
--header "Content-Type: application/json" \
--data '{"bindings":{"1":"Jane","2":"ORD-1001"}}'HTTP 201:
{
"status": 201,
"message": "Success",
"timestamp": "2026-09-09T10:16:00.000Z",
"path": "/v1/waba-api/templates/b3f1c2a0-1234-4a5b-9c6d-7e8f9a0b1c2d/preview",
"data": {
"channel": "whatsapp",
"renderedComponents": [
{
"type": "body",
"text": "Hello Jane, your order ORD-1001 is ready.",
"variables": ["1", "2"]
}
]
}
}Common failures: malformed UUID (400), template absent or not owned (404).
Next: check provider status before sending.
Synchronize Template
POST /v1/waba-api/templates/:templateId/sync
Requests a provider projection refresh. It does not submit a draft. The immediate response can still contain the pre-refresh sync fields; retrieve status afterward.
curl --request POST \
"$NOTIFY_API_BASE_URL/v1/waba-api/templates/b3f1c2a0-1234-4a5b-9c6d-7e8f9a0b1c2d/sync" \
--header "Authorization: Bearer $NOTIFY_WABA_API_KEY"Success: HTTP 201; data contains the complete template response.
Common failures: malformed UUID (400), non-WhatsApp template (400), template absent/not owned (404), provider or connection failure.
Next: call the status endpoint or process template.status.updated.
Get Template Status
GET /v1/waba-api/templates/:templateId/status
curl --request GET \
"$NOTIFY_API_BASE_URL/v1/waba-api/templates/b3f1c2a0-1234-4a5b-9c6d-7e8f9a0b1c2d/status" \
--header "Authorization: Bearer $NOTIFY_WABA_API_KEY"HTTP 200:
{
"status": 200,
"message": "Success",
"timestamp": "2026-09-09T10:18:00.000Z",
"path": "/v1/waba-api/templates/b3f1c2a0-1234-4a5b-9c6d-7e8f9a0b1c2d/status",
"data": {
"id": "b3f1c2a0-1234-4a5b-9c6d-7e8f9a0b1c2d",
"status": "approved",
"isActive": true,
"syncStatus": "synced",
"lastSyncedAt": "2026-09-09T10:17:30.000Z",
"lastSyncError": null
}
}Unknown provider states are exposed as pending in data.status. Retrieve the full template to inspect raw whatsappConfig.providerStatus and statusInfo.
Next: send when status is approved and isActive is true.
Update Template
PATCH /v1/waba-api/templates/:templateId
Mutable catalog fields: name, description, and isActive. Provider fields: externalTemplateCategory, parameterFormat, and componentSchema.
curl --request PATCH \
"$NOTIFY_API_BASE_URL/v1/waba-api/templates/b3f1c2a0-1234-4a5b-9c6d-7e8f9a0b1c2d" \
--header "Authorization: Bearer $NOTIFY_WABA_API_KEY" \
--header "Content-Type: application/json" \
--data '{
"name": "Order Update v2",
"description": "Updated order notification"
}'HTTP 200; data contains the updated template.
Changing provider fields validates and resubmits the template, then resets its public state to pending. The external template name and language are not update fields.
Common failures: invalid component schema (400), template absent/not owned (404), provider rejection or unavailable dependency.
Next: wait for approval again after a provider-field update.
Delete Template
DELETE /v1/waba-api/templates/:templateId
curl --request DELETE \
"$NOTIFY_API_BASE_URL/v1/waba-api/templates/b3f1c2a0-1234-4a5b-9c6d-7e8f9a0b1c2d" \
--header "Authorization: Bearer $NOTIFY_WABA_API_KEY"Success: HTTP 200 after provider deletion and local deactivation/soft deletion.
Common failures: malformed UUID (400), template absent/not owned (404), provider deletion failure.
Next: remove the external template name from sending configuration.
Send an Approved Template
Use whatsappConfig.externalTemplateName as template_name in POST /v1/waba-api/messages/template. The send worker performs provider/template preflight asynchronously, so preserve the message result and process later status events.
