Skip to main content

Message Templates

Create, list, delete and sync WhatsApp message templates, including authentication templates and the AI drafting endpoint.

A message template is pre-approved copy you can send outside the 24-hour customer service window. Order updates, delivery notices, reminders and one-time codes are all template sends. Templates are created on WhatsApp, reviewed by Meta, and mirrored locally so you can list and filter them without a Meta round trip.

All endpoints are under https://api.callmissed.com/api/v1/whatsapp.

Lifecycle

  1. 1

    Create

    validates the copy locally, then submits it to WhatsApp

  2. 2

    Review

    Meta reviews it. The template sits at

  3. 3

    Approved

    A status webhook flips it to and it becomes sendable

Statuses you will see: PENDING, APPROVED, REJECTED, PAUSED, DISABLED, IN_APPEAL. Only APPROVED templates can be sent. A rejected template carries a rejection_reason.

Choosing the WABA

Template endpoints act on a WhatsApp Business Account rather than a phone number. Supply exactly one:

FieldTypeWhere it comes from
account_idUUIDThe id from GET /accounts
waba_idstring, max 64Meta's WABA id

Omitting both returns 400 with "Either account_id (UUID) or waba_id (Meta) is required". On GET /templates these are optional filters instead.

Create a template

POST /api/v1/whatsapp/templates · scope whatsapp:write

FieldTypeRequiredNotes
account_id / waba_idUUID / stringOne ofThe WABA to create under
namestring, 1 to 512 charsYesMust match ^[a-z0-9_]+$: lowercase letters, digits and underscores only
categorystringYesMARKETING, UTILITY or AUTHENTICATION
languagestring, 2 to 12 charsYesLocale, for example en_US, hi, es_MX
componentsarray of objects, at least 1YesHeader, body, footer and button spec. Must include a BODY
parameter_formatstringNoPOSITIONAL or NAMED, selecting the variable syntax
allow_category_changebooleanNoLet Meta re-categorise the template. Defaults to on, so only send this to opt out
curl -X POST https://api.callmissed.com/api/v1/whatsapp/templates \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "waba_id": "102290129340398",
    "name": "order_shipped",
    "category": "UTILITY",
    "language": "en_US",
    "components": [
      { "type": "HEADER", "format": "TEXT", "text": "Your order is on its way" },
      {
        "type": "BODY",
        "text": "Hi {{1}}, order {{2}} shipped today and should arrive in 2 to 3 days.",
        "example": { "body_text": [["Priya", "AC-10294"]] }
      },
      { "type": "FOOTER", "text": "Acme Coffee" }
    ]
  }'

components is forwarded to WhatsApp unchanged, so any component type WhatsApp supports works, including button blocks. BODY, FOOTER, text HEADER and the three marketing formats below (carousel, limited-time offer, coupon code) are checked locally first; everything else is validated by Meta.

Response (200 OK)

{
  "template_id": "1234567890123456",
  "status": "PENDING",
  "template": {
    "id": "3f9a1c20-7d8e-4b1a-9c2f-5e6a7b8c9d0e",
    "account_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "template_id": "1234567890123456",
    "name": "order_shipped",
    "language": "en_US",
    "category": "UTILITY",
    "status": "PENDING",
    "quality_score": "UNKNOWN",
    "rejection_reason": null,
    "components": [],
    "last_meta_synced_at": null,
    "created_at": "2026-04-19T12:00:00Z",
    "updated_at": "2026-04-19T12:00:00Z"
  }
}
FieldTypeNotes
template_idstring, nullableMeta's template id
statusstringInitial lifecycle state, typically PENDING
templateobjectThe mirrored row, described in the template object

The local row is written only after WhatsApp accepts the create, so a rejection leaves nothing behind.

Validation before submission

Copy is checked locally first, so a guaranteed rejection does not cost a Meta round trip. Each of these returns 400 with the reason:

RuleMessage you get
Name outside ^[a-z0-9_]+$template name must match ^[a-z0-9_]+$ (lowercase letters, digits, and underscores only)
No BODY componentA BODY component is required.
Empty BODY textThe BODY component requires non-empty text.
BODY over 1024 charactersBODY text exceeds 1024 characters.
FOOTER over 60 charactersFOOTER text exceeds 60 characters.
{{N}} variables with no exampleA component with {{N}} variables requires an 'example.body_text' array.
Example count does not match the variable countThe example provides 1 value(s) but the text has 2 {{N}} variable(s).
An example on a component with no variablesOmit the 'example' object on a component with no {{N}} variables -- Meta rejects an empty example.

example.body_text is an array of arrays: one inner array holding a sample value per variable. A text HEADER with variables uses example.header_text, a flat array.

Authentication templates

One-time-code templates have a different body shape. Meta owns the verification copy, so you must not send BODY text:

curl -X POST https://api.callmissed.com/api/v1/whatsapp/templates \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "waba_id": "102290129340398",
    "name": "acme_login_code",
    "category": "AUTHENTICATION",
    "language": "en_US",
    "components": [
      { "type": "BODY", "add_security_recommendation": true }
    ]
  }'

The only body option is the boolean add_security_recommendation, and there is no example because there is no sender-supplied variable in the body. Sending BODY text on an AUTHENTICATION template returns 400 telling you the verification-code copy is fixed by Meta, and a non-boolean add_security_recommendation returns 400 as well. Any additional button or footer options come from Meta's authentication-template reference and are passed through unchanged.

Once approved, send the code through POST /messages/template, passing it as the body or button parameter.

A carousel pairs a normal message BODY with a swipeable row of cards, each with its own media header and buttons. Add a CAROUSEL component alongside the BODY.

Carousels are MARKETING only. Under any other category the create returns 400 naming the format.

RuleDetail
cards2 to 10. The count is fixed at creation: an approved template can only send the number of cards it was created with
Card HEADERRequired on every card, and always media. format is IMAGE or VIDEO
Card header mediaexample.header_handle must be a non-empty array holding an uploaded media handle
Card BODYOptional, but if one card has it every card must. Text max 160 characters, far shorter than the 1024-character message body. Variables need an example object
Card BUTTONSOptional, at most 2 per card, of type QUICK_REPLY, URL or PHONE_NUMBER
Uniform structureEvery card must carry the same components in the same order, and the same button types. Cards render at a shared height, so a body or button on one card is required on all
Top-level BODYStill required, alongside the CAROUSEL component
curl -X POST https://api.callmissed.com/api/v1/whatsapp/templates \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "waba_id": "102290129340398",
    "name": "summer_blends_carousel",
    "category": "MARKETING",
    "language": "en_US",
    "components": [
      {
        "type": "BODY",
        "text": "Hi {{1}}, our cold brew blends are 20% off this week.",
        "example": { "body_text": [["Priya"]] }
      },
      {
        "type": "CAROUSEL",
        "cards": [
          {
            "components": [
              {
                "type": "HEADER",
                "format": "IMAGE",
                "example": { "header_handle": ["4::aW1hZ2UvanBlZw==:ARZ1"] }
              },
              { "type": "BODY", "text": "Ratnagiri Dark Roast, notes of cocoa and dried fig." },
              {
                "type": "BUTTONS",
                "buttons": [
                  { "type": "QUICK_REPLY", "text": "Send me a sample" },
                  { "type": "URL", "text": "Shop now", "url": "https://acme.example.com/dark-roast" }
                ]
              }
            ]
          },
          {
            "components": [
              {
                "type": "HEADER",
                "format": "IMAGE",
                "example": { "header_handle": ["4::aW1hZ2UvanBlZw==:ARZ2"] }
              },
              { "type": "BODY", "text": "Chikmagalur Medium Roast, bright and citrus-forward." },
              {
                "type": "BUTTONS",
                "buttons": [
                  { "type": "QUICK_REPLY", "text": "Send me a sample" },
                  { "type": "URL", "text": "Shop now", "url": "https://acme.example.com/medium-roast" }
                ]
              }
            ]
          }
        ]
      }
    ]
  }'

Every rule above is checked before submission, and the 400 names the card index and the field, so you do not have to reverse-engineer a generic rejection.

Limited-time offer templates

A limited-time offer adds an offer banner with an optional countdown. Add a LIMITED_TIME_OFFER component. MARKETING only.

RuleDetail
limited_time_offerRequired object: { "text": string (max 16), "has_expiration": boolean }. text is the offer label
BODYMax 600 characters on this format, stricter than the usual 1024
HEADEROptional, but when present must be IMAGE or VIDEO. A text header is not supported
FOOTERNot supported at all. Sending one returns 400
BUTTONSOnly COPY_CODE and URL. When both are present the COPY_CODE button must be declared first, because it is fixed at button index 0 and the URL button at index 1
curl -X POST https://api.callmissed.com/api/v1/whatsapp/templates \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "waba_id": "102290129340398",
    "name": "monsoon_offer",
    "category": "MARKETING",
    "language": "en_US",
    "components": [
      {
        "type": "HEADER",
        "format": "IMAGE",
        "example": { "header_handle": ["4::aW1hZ2UvanBlZw==:ARZ1"] }
      },
      {
        "type": "BODY",
        "text": "Hi {{1}}, take 20% off your next bag of coffee.",
        "example": { "body_text": [["Priya"]] }
      },
      {
        "type": "LIMITED_TIME_OFFER",
        "limited_time_offer": { "text": "20% off", "has_expiration": true }
      },
      {
        "type": "BUTTONS",
        "buttons": [
          { "type": "COPY_CODE", "example": "MONSOON20" },
          { "type": "URL", "text": "Shop now", "url": "https://acme.example.com/shop" }
        ]
      }
    ]
  }'

has_expiration: true renders a countdown, whose expiry is supplied per send as a component parameter on POST /messages/template. components is forwarded to WhatsApp unchanged on a template send, so the parameter shape is WhatsApp's own.

Coupon code templates

A COPY_CODE button gives the customer a one-tap copy of a discount code. It works on its own marketing template, and is also the button an LTO template uses. MARKETING only.

RuleDetail
Button shape{ "type": "COPY_CODE", "example": "<CODE>" }. The button's label is fixed, so there is no text to set
exampleRequired, a sample coupon code, max 20 characters. The same cap applies to the code you pass at send time
CountAt most one COPY_CODE button per template
CompanionsA QUICK_REPLY button may accompany it
curl -X POST https://api.callmissed.com/api/v1/whatsapp/templates \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "waba_id": "102290129340398",
    "name": "welcome_coupon",
    "category": "MARKETING",
    "language": "en_US",
    "components": [
      {
        "type": "BODY",
        "text": "Welcome to Acme, {{1}}. Here is 15% off your first order.",
        "example": { "body_text": [["Priya"]] }
      },
      {
        "type": "BUTTONS",
        "buttons": [
          { "type": "COPY_CODE", "example": "WELCOME15" },
          { "type": "QUICK_REPLY", "text": "Browse blends" }
        ]
      }
    ]
  }'

The example is a sample for review, not the code you ship. The real code goes in a coupon_code button parameter per send, capped at the same 20 characters, so one approved template can issue a different code to every customer.

An AUTHENTICATION template's { "type": "OTP", "otp_type": "COPY_CODE" } button is a different component on a different template family and is not subject to these rules.

List templates

GET /api/v1/whatsapp/templates · scope whatsapp:read

Reads the local mirror, newest updated first. All parameters are optional filters.

ParamTypeDefaultNotes
account_idUUIDnoneFilter to one connected account
waba_idstring, max 64noneFilter by Meta WABA id
statusstringnoneAPPROVED, PENDING, REJECTED, PAUSED, DISABLED, IN_APPEAL. Case-insensitive
categorystringnoneMARKETING, UTILITY or AUTHENTICATION. An unknown value returns 400
languagestring, max 12noneFilter by locale
limitinteger, 1 to 500100Max rows
curl "https://api.callmissed.com/api/v1/whatsapp/templates?status=APPROVED&limit=100" \
  -H "Authorization: Bearer cm_your_api_key"

Response (200 OK)

[
  {
    "id": "3f9a1c20-7d8e-4b1a-9c2f-5e6a7b8c9d0e",
    "account_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "template_id": "1234567890123456",
    "name": "order_shipped",
    "language": "en_US",
    "category": "UTILITY",
    "status": "APPROVED",
    "quality_score": "GREEN",
    "rejection_reason": null,
    "components": [
      { "type": "BODY", "text": "Hi {{1}}, order {{2}} shipped today and should arrive in 2 to 3 days." },
      { "type": "FOOTER", "text": "Acme Coffee" }
    ],
    "last_meta_synced_at": "2026-04-19T13:02:44Z",
    "created_at": "2026-04-19T12:00:00Z",
    "updated_at": "2026-04-19T13:02:44Z"
  }
]

The mirror is kept current by status webhooks and an hourly reconciliation sweep. For up-to-the-second consistency, call sync first.

The template object

FieldTypeNotes
idUUIDCallMissed's id. Use it on get and delete
account_idUUIDThe owning WABA
template_idstring, nullableMeta's template id. Null if Meta never confirmed the create
namestringTemplate name
languagestringLocale
categorystringMARKETING, UTILITY or AUTHENTICATION
statusstringLifecycle state
quality_scorestringMeta's quality signal, for example GREEN or UNKNOWN
rejection_reasonstring, nullableWhy Meta rejected it
componentsarray of objectsThe approved component spec
last_meta_synced_atdatetime, nullableLast reconciliation against Meta
created_at / updated_atdatetimeISO 8601 UTC

Get one template

GET /api/v1/whatsapp/templates/{template_uuid} · scope whatsapp:read

{template_uuid} is the id field, not Meta's template_id. Returns the template object, or 404 if it is not on your workspace.

curl https://api.callmissed.com/api/v1/whatsapp/templates/3f9a1c20-7d8e-4b1a-9c2f-5e6a7b8c9d0e \
  -H "Authorization: Bearer cm_your_api_key"

Delete a template

DELETE /api/v1/whatsapp/templates/{template_uuid} · scope whatsapp:write

Deletes on WhatsApp and drops the local row. Returns 204 No Content with an empty body.

curl -X DELETE https://api.callmissed.com/api/v1/whatsapp/templates/3f9a1c20-7d8e-4b1a-9c2f-5e6a7b8c9d0e \
  -H "Authorization: Bearer cm_your_api_key"

If the template was already deleted in WhatsApp Manager, the local row is cleaned up anyway. A template that never got a Meta id is simply dropped locally.

Deleting an approved template starts a 30-day cooldown before the same name can be reused. Reusing it sooner fails at create time.

Sync from WhatsApp

POST /api/v1/whatsapp/templates/sync · scope whatsapp:write

Pulls every template for a WABA from WhatsApp and upserts the local mirror. An hourly sweep does this automatically, so call it when you have just edited templates in WhatsApp Manager and want them reflected immediately.

FieldTypeRequiredNotes
account_id / waba_idUUID / stringOne ofThe WABA to reconcile
curl -X POST https://api.callmissed.com/api/v1/whatsapp/templates/sync \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "waba_id": "102290129340398" }'

Response (200 OK)

{ "waba_id": "102290129340398", "fetched": 12, "inserted": 2, "updated": 10 }
FieldTypeNotes
waba_idstringThe WABA that was reconciled
fetchedintegerTemplates WhatsApp returned
insertedintegerNew local rows
updatedintegerExisting rows refreshed

Templates WhatsApp no longer returns are not deleted by this call. The background sweep owns that.

Draft a template with AI

POST /api/v1/whatsapp/ai/draft_template · scope whatsapp:read

Turns a plain-language intent into a Meta-compliant draft, with an approval-risk assessment. It is read-only: nothing is submitted to WhatsApp, so review the draft and then post it to create yourself. The generation is billed to your workspace.

FieldTypeRequiredNotes
intentstring, 10 to 1000 charsYesWhat the template should say and when it is sent
languagestring, 2 to 8 charsNoDefault en
categorystringNoForce UTILITY, MARKETING or AUTHENTICATION. Omit to let the model choose
emojisbooleanNoAllow emojis in the body. Default false, which is safest for approval
include_headerbooleanNoAllow a header line. Default true
include_footerbooleanNoAllow a footer line. Default true
tonestring, max 40NoFor example friendly, formal, concise
curl -X POST https://api.callmissed.com/api/v1/whatsapp/ai/draft_template \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "intent": "Tell a customer their coffee subscription renews in three days and they can skip or change the blend before then.",
    "language": "en",
    "category": "UTILITY",
    "tone": "friendly"
  }'

Response (200 OK)

{
  "name": "subscription_renewal_reminder",
  "category": "UTILITY",
  "language": "en",
  "body": "Hi {{1}}, your Acme coffee subscription renews on {{2}}. Reply SKIP to pause this delivery or CHANGE to pick a different blend.",
  "header_text": "Your subscription renews soon",
  "footer_text": "Acme Coffee",
  "components": [
    { "type": "HEADER", "format": "TEXT", "text": "Your subscription renews soon" },
    {
      "type": "BODY",
      "text": "Hi {{1}}, your Acme coffee subscription renews on {{2}}. Reply SKIP to pause this delivery or CHANGE to pick a different blend.",
      "example": { "body_text": [["Priya", "22 April"]] }
    },
    { "type": "FOOTER", "text": "Acme Coffee" }
  ],
  "approval_risk": "low",
  "rejection_risks": [],
  "compliance_notes": "Transactional reminder tied to an existing subscription, so UTILITY is the correct category."
}
FieldTypeNotes
namestringSuggested template name, already matching Meta's naming rules
categorystringThe category chosen or forced
languagestringEchoes the requested locale
body / header_text / footer_textstring, nullable for header and footerThe drafted copy
componentsarray of objectsReady to post to POST /templates as-is
approval_riskstringThe model's read on how likely Meta is to approve it
rejection_risksarray of stringsSpecific things that could get it rejected. Empty when none were found
compliance_notesstring, nullableWhy the category and wording were chosen

422 when the model cannot produce a valid draft. Shorten or clarify the intent and retry.