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.

With an app key, creating, editing and uploading need the whatsapp:templates scope; listing needs only whatsapp. With your org API key you can also delete templates and see every status, including rejected ones. Sending an already-approved template is a separate call (/send-template) covered in Template messages.


Before you start

Get an app key that holds whatsapp:templates (see Scopes) and find your channel id — every template lives on the WhatsApp Business Account behind one Meta Cloud 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", "kind": "cloud" }
] } }

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

Authorization: Bearer vai_app_xxxxxxxxxxxxxxxx

Templates work on Meta Cloud numbers — native ("WhatsApp Business, set up for you") or connected through your own Meta app. On your own Meta app, a media header also needs the connection's Meta App ID saved, or the upload returns 400. With your org API key, use the org routes instead.


Create a utility template

POST /channels/{id}/templates submits a template definition to Meta for review and mirrors it locally with the status Meta returns — normally 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, at least one. Meta’s definition array (see below). Meta expects an example for each {{n}}
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"
} } }
  • The BODY component is optional; if you include it, add_security_recommendation must be true or false (it appends Meta’s “For your security, don’t share this code” line), and it can't carry text.
  • The FOOTER needs code_expiration_minutes (1–90), which shows the customer when the code expires and greys out the copy button afterwards.
  • Exactly one OTP button, and only COPY_CODE — one-tap autofill needs Android app-signing details that aren’t collected yet. No media header.

When you send this template, WhatsApp needs the code twice — once for the body and once for the copy-code button. On a Meta Cloud number, pass it once as variables: ["482913"] and Telenow puts it on the button too (see one-time-passcode templates). If you send components, they go out as written, so include both parts:

"components": [
  { "type": "body", "parameters": [ { "type": "text", "text": "482913" } ] },
  { "type": "button", "sub_type": "url", "index": 0, "parameters": [ { "type": "text", "text": "482913" } ] }
]

Approval & status

A create normally returns status: "PENDING". Meta review usually lands within minutes and moves the template to APPROVED, REJECTED or PAUSED (later statuses also include DISABLED, IN_APPEAL and PENDING_DELETION). 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. The list is refreshed from Meta at most every 10 minutes, so an approval can take that long to show.
  2. See every status with your org API key. GET /api/orgs/{orgId}/whatsapp-cloud/channels/{channelId}/templates re-reads Meta on every call and returns all templates — pending, rejected and paused included — with rejected_reason and quality_score. See With your org API key.
  3. No webhook for template status. 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.

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 checks these rules before forwarding to Meta, so a malformed template fails 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 text≤ 60 chars
FOOTER text≤ 60 chars
Button text≤ 25 chars each
Buttons≤ 10 total
VariablesContiguous {{1}}…{{N}} (no gaps) in the BODY text

Authentication templates follow their own rules instead (see above); the length limits don't apply to them. Telenow doesn't check example values or header/URL variables — Meta does, and its refusal comes back as 400 with Meta's explanation.


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; a PENDING one returns 400 only approved, rejected or paused templates can be edited. components is required in every edit — a category-only change is refused. 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": [] }
] } }

variables lists the {{n}} placeholders in the template's body text, in order — pass one variables string per index when you send. An authentication template has no body text of its own, so its list can be empty — it still takes exactly one value, the code: send variables: ["<code>"] on a Meta Cloud number, or components (see above). Templates still PENDING or REJECTED are not returned here; use the org-key list to see those.


Endpoint reference

Base /api/app-whatsapp, Meta Cloud numbers. Rate-limited ~20 req/s (burst 60) per app, plus the shared per-IP limit (see Errors & limits); 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 supported yet: carousel and limited-time-offer templates (Telenow doesn't check or send them), and one-tap OTP.


With your org API key

With your org API key (X-API-Key: vai_live_…), template management lives under /api/orgs/{orgId}/whatsapp-cloud/channels/{channelId}/templates. It also works on Meta Cloud numbers only (other numbers return 404). Two differences to watch: the create body uses snake_case allow_category_change (a camelCase allowCategoryChange is silently ignored here), and responses return the template as a snake_case row.

MethodPath (under …/whatsapp-cloud/channels/{channelId})What it doesKey's creator
GET/templatesEvery template — any status — re-read from Meta → { templates, plane, supportsMediaHeader, statusNote, complete, syncError }any member
POST/templatesCreate → { template }. Body { name, language, category, components, allow_category_change? }owner/admin
POST/templates/mediaUpload a header sample (multipart file, same types, 16 MB) → { handle }owner/admin
PATCH/templates/{templateId}Edit and resubmit → { template }. Body { components, category? }owner/admin
DELETE/templates/{templateId}Delete — every language of that template name → { success, message }owner/admin

Each row carries id, name, language, category, status, quality_score, rejected_reason, components, variables, meta_template_id, waba_id and timestamps. {templateId} is that row's id. Delete removes the template at Meta and in Telenow; if Meta's delete fails, the Telenow copy is removed anyway.

To list what a number can send — on every kind of number, including partner numbers — use GET /api/orgs/{orgId}/whatsapp-hub/channels/{channelId}/templates (see Template messages). Writes need an owner, admin or developer key.