Creating & managing templates

Creating & managing templates

Templates are the pre-approved messages that let you start a conversation and message a customer outside the 24-hour window. This page covers creating templates (utility, marketing, media-header and authentication/OTP), uploading header sample media, editing, listing and the validation rules — over the app-key API.

Everything here needs the whatsapp:templates scope on your app key. Sending an already-approved template is a separate call (/send-template) covered in the WhatsApp API.


Before you start

Get an app key that holds whatsapp:templates (see Scopes) and find your channel id — every template lives on one native channel (your connected WhatsApp number):

curl https://api.telenow.ai/api/app-whatsapp/channels \
  -H "Authorization: Bearer vai_app_…"
{ "success": true, "data": { "channels": [
  { "id": "b1e2c3d4-…", "label": "Acme Support", "phone": "+14155550100" }
] } }

Use that id as the {id} in the paths below. Send the key on every request:

Authorization: Bearer vai_app_xxxxxxxxxxxxxxxx

Templates work on native channels only (numbers connected through the “WhatsApp Business, set up for you” path). The dashboard equivalent is POST /api/orgs/{orgId}/whatsapp-cloud/channels/{cid}/templates with a user JWT.


Create a utility template

POST /channels/{id}/templates submits a template definition to Meta for review and mirrors it locally as PENDING. The body is camelCase:

FieldTypeNotes
namestringRequired. Lowercase letters, digits and _ only, ≤ 512 chars. Immutable after creation
languagestringRequired. BCP-47 tag, e.g. en_US, en_GB, hi
categorystringRequired. One of MARKETING, UTILITY, AUTHENTICATION
componentsarrayRequired. Meta’s definition array (see below). Each {{n}} needs an example
allowCategoryChangeboolOptional. Let Meta re-categorise (e.g. UTILITY → MARKETING) instead of rejecting

A utility template with two body variables — note the example.body_text (a list of one row of sample values, one per {{n}}):

curl -X POST https://api.telenow.ai/api/app-whatsapp/channels/b1e2c3d4-…/templates \
  -H "Authorization: Bearer vai_app_…" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "order_update",
    "language": "en_US",
    "category": "UTILITY",
    "components": [
      { "type": "BODY",
        "text": "Hi {{1}}, your order {{2}} has shipped and will arrive soon.",
        "example": { "body_text": [["Asha", "#1234"]] } }
    ]
  }'
{ "success": true, "data": { "template": {
  "id": "7c1a9f22-…",
  "name": "order_update",
  "language": "en_US",
  "category": "UTILITY",
  "status": "PENDING",
  "qualityScore": null,
  "rejectedReason": null,
  "metaTemplateId": "1443219876543210"
} } }

The template is now PENDING at Meta. See Approval & status for how it becomes APPROVED.


Create a marketing template

A richer template: a text header with a variable, a body, a footer, and a full set of buttons — a dynamic URL, a QUICK_REPLY, a PHONE_NUMBER, and a COPY_CODE coupon. Every variable ({{n}}) in the header, body and URL carries its own example:

curl -X POST https://api.telenow.ai/api/app-whatsapp/channels/b1e2c3d4-…/templates \
  -H "Authorization: Bearer vai_app_…" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "spring_sale",
    "language": "en_US",
    "category": "MARKETING",
    "components": [
      { "type": "HEADER", "format": "TEXT", "text": "{{1}} sale is live",
        "example": { "header_text": ["Summer"] } },
      { "type": "BODY",
        "text": "Hi {{1}}, enjoy 20% off everything this week. Use your code at checkout.",
        "example": { "body_text": [["Asha"]] } },
      { "type": "FOOTER", "text": "Reply STOP to opt out" },
      { "type": "BUTTONS", "buttons": [
        { "type": "URL", "text": "Shop now", "url": "https://shop.example.com/{{1}}",
          "example": ["spring"] },
        { "type": "QUICK_REPLY", "text": "No thanks" },
        { "type": "PHONE_NUMBER", "text": "Call us", "phone_number": "+14155550100" },
        { "type": "COPY_CODE", "example": "SAVE20" }
      ] }
    ]
  }'
{ "success": true, "data": { "template": {
  "id": "9d33be07-…",
  "name": "spring_sale",
  "language": "en_US",
  "category": "MARKETING",
  "status": "PENDING",
  "qualityScore": null,
  "rejectedReason": null,
  "metaTemplateId": "1443219876500000"
} } }

Button notes:

  • URL — static, or dynamic with a trailing {{1}} in the url plus an example value for the variable part.
  • QUICK_REPLY — a tappable reply; the tap arrives back to you as a button reply on the whatsapp.message.received webhook.
  • PHONE_NUMBER — dials phone_number (E.164).
  • COPY_CODE — a coupon button; example is a sample code. (This is the marketing coupon copy-code button — different from the OTP copy-code used in authentication templates.)

Create a media-header template

For an image / video / document header, Meta needs a sample asset at definition time. That’s a two-step flow: upload the sample to get a handle, then reference it in the header’s example.header_handle.

Step 1 — upload the sample asset. POST /channels/{id}/templates/media takes a multipart/form-data body with a single file field. Accepts image/jpeg, image/png, video/mp4, application/pdf, up to 16 MB:

curl -X POST https://api.telenow.ai/api/app-whatsapp/channels/b1e2c3d4-…/templates/media \
  -H "Authorization: Bearer vai_app_…" \
  -F "[email protected]"
{ "success": true, "data": { "handle": "4::aW1hZ2UvcG5n:ARZ…" } }

Step 2 — create the template with the returned handle in an IMAGE (or VIDEO / DOCUMENT) header:

curl -X POST https://api.telenow.ai/api/app-whatsapp/channels/b1e2c3d4-…/templates \
  -H "Authorization: Bearer vai_app_…" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "receipt_with_logo",
    "language": "en_US",
    "category": "UTILITY",
    "components": [
      { "type": "HEADER", "format": "IMAGE",
        "example": { "header_handle": ["4::aW1hZ2UvcG5n:ARZ…"] } },
      { "type": "BODY",
        "text": "Hi {{1}}, here is your receipt for order {{2}}.",
        "example": { "body_text": [["Asha", "#1234"]] } },
      { "type": "FOOTER", "text": "Thanks for shopping with Acme" }
    ]
  }'
{ "success": true, "data": { "template": {
  "id": "2a71c0e5-…", "name": "receipt_with_logo", "language": "en_US",
  "category": "UTILITY", "status": "PENDING",
  "qualityScore": null, "rejectedReason": null, "metaTemplateId": "1443219876511111"
} } }

Use "format": "VIDEO" (upload an mp4) or "format": "DOCUMENT" (upload a pdf) for those header types. When you later send this template you supply the runtime asset — the mediaId from a normal media upload — in the header parameter; see Templates with a media header.


Create an authentication (OTP) template

Authentication templates use a fixed shape — you don’t write the body text. Meta generates the OTP wording; you only toggle the security recommendation, set the code expiry, and add the copy-code button. A custom body would be rejected.

curl -X POST https://api.telenow.ai/api/app-whatsapp/channels/b1e2c3d4-…/templates \
  -H "Authorization: Bearer vai_app_…" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "login_code",
    "language": "en_US",
    "category": "AUTHENTICATION",
    "components": [
      { "type": "BODY", "add_security_recommendation": true },
      { "type": "FOOTER", "code_expiration_minutes": 5 },
      { "type": "BUTTONS", "buttons": [
        { "type": "OTP", "otp_type": "COPY_CODE", "text": "Copy code" }
      ] }
    ]
  }'
{ "success": true, "data": { "template": {
  "id": "5f0d84a1-…", "name": "login_code", "language": "en_US",
  "category": "AUTHENTICATION", "status": "PENDING",
  "qualityScore": null, "rejectedReason": null, "metaTemplateId": "1443219876522222"
} } }
  • add_security_recommendation: true appends Meta’s “For your security, don’t share this code” line.
  • code_expiration_minutes (1–90) shows the customer when the code expires and greys out the copy button afterwards.
  • Only COPY_CODE OTP buttons are supported today. One-tap autofill needs Android app-signing details that aren’t collected yet, so it’s not available.

When you send this template, the code is your first variables value (e.g. "variables": ["482913"]), which fills both the body and the copy-code button.


Approval & status

Every create returns status: "PENDING". Meta review usually lands within minutes and moves the template to APPROVED, REJECTED, or PAUSED. How to observe it:

  1. Poll the list endpoint. GET /channels/{id}/templates returns only approved templates — once your template appears there, it’s live.
  2. No webhook for template status. There is no webhook for template status changes — whatsapp.account.health fires only for account/number quality and review outcomes (account_update, phone_number_quality_update, account_review_update), never for template approval/rejection/pause. Because GET /channels/{id}/templates returns only APPROVED templates, a REJECTED or PAUSED transition can’t be observed via the API — check the dashboard’s Message templates view for those.

The create/edit response echoes the live status, plus qualityScore (Meta’s green/yellow/red rating, null until it has traffic) and rejectedReason (populated when status is REJECTED, e.g. a policy or formatting reason).


Validation limits

The server validates before forwarding to Meta, so malformed templates fail with a clear 400 instead of an opaque Meta rejection:

RuleLimit
nameLowercase letters, digits, _; ≤ 512 chars
categoryMARKETING | UTILITY | AUTHENTICATION
BODYExactly one non-empty BODY (marketing/utility); text ≤ 1024
HEADER text60 chars
FOOTER text60 chars
Button text25 chars each
Buttons10 total
VariablesContiguous {{1}}{{N}} (no gaps); each needs an example

Authentication templates skip the BODY-text rule (the shape is fixed). A missing or non-contiguous variable, an over-long field, or a missing example all return 400 with the offending field named.


Edit a template

PATCH /templates/{templateId} updates an existing template’s components (and optionally category). name and language are immutable at Meta — you can’t change them; create a new template instead. Editing re-submits for review, so the template returns to PENDING.

curl -X PATCH https://api.telenow.ai/api/app-whatsapp/templates/7c1a9f22-… \
  -H "Authorization: Bearer vai_app_…" \
  -H "Content-Type: application/json" \
  -d '{
    "components": [
      { "type": "BODY",
        "text": "Hi {{1}}, your order {{2}} is out for delivery today.",
        "example": { "body_text": [["Asha", "#1234"]] } }
    ]
  }'
{ "success": true, "data": { "template": {
  "id": "7c1a9f22-…", "name": "order_update", "language": "en_US",
  "category": "UTILITY", "status": "PENDING",
  "qualityScore": null, "rejectedReason": null, "metaTemplateId": "1443219876543210"
} } }

Approved, rejected and paused templates can all be edited in place. The same validation limits apply.


List approved templates

GET /channels/{id}/templates returns the channel’s approved templates and, for each, how many variables it takes — everything you need to call /send-template:

curl https://api.telenow.ai/api/app-whatsapp/channels/b1e2c3d4-…/templates \
  -H "Authorization: Bearer vai_app_…"
{ "success": true, "data": { "templates": [
  { "name": "order_update", "language": "en_US", "category": "UTILITY",
    "variables": [ { "index": 1 }, { "index": 2 } ] },
  { "name": "login_code", "language": "en_US", "category": "AUTHENTICATION",
    "variables": [ { "index": 1 } ] }
] } }

variables lists the body placeholders in order — pass one variables string per index when you send. Templates still PENDING or REJECTED are not returned here; use the dashboard Message templates view to see those.


Endpoint reference

Base /api/app-whatsapp, native channels only. Rate-limited ~20 req/s (burst 60); template-media upload capped at 16 MB.

MethodPathScopePurpose
GET/channels/{id}/templateswhatsappList approved templates
POST/channels/{id}/templateswhatsapp:templatesCreate a template → { template }
POST/channels/{id}/templates/mediawhatsapp:templatesUpload a header sample → { handle }
PATCH/templates/{tid}whatsapp:templatesEdit a template → { template }

Not built yet: carousel and limited-time-offer template types, and one-tap OTP.