API WhatsApp

Message Templates

WhatsApp message template categories, Meta billing rules, components, limits and button types, the templates API, and OTP via authentication templates.

A message template is a message format Meta approves in advance. Templates are the only messages you can send to start a conversation, or to reach a customer after their 24-hour window has closed.

  • Templates are created and submitted in the dashboard (Templates → Create template). Meta reviews every one.
  • The API is read-only for templates: list them, read their status and components, and send approved ones with POST /api/v1/messages.

Categories and Meta billing

Every template has one of three categories. Meta sets the price by category and bills it directly to your WhatsApp Business Account. StartMessaging never charges per message.

CategoryUse it forBilled by Meta
MarketingPromotions, offers, announcements, re-engagementEvery delivered message
UtilityUpdates about something the customer started: orders, bookings, payments, account alertsEvery delivered message, including inside an open customer service window
AuthenticationOne-time passwords and login codesEvery delivered message

Since 1 October 2026, Meta charges every delivered template, utility templates included, even inside the 24-hour customer service window. The one exception is a free entry point window (below).

  • Service replies (free-form text, media and interactive messages inside the 24-hour window) are free up to a monthly allowance per phone number, then Meta charges them.
  • When a customer messages you from a Click-to-WhatsApp ad or a Facebook Page call-to-action button and you reply within 24 hours, a free entry point window of 72 hours opens. Every message inside it is free, templates included.

Each sent message’s pricing field shows how Meta classified it (see Send Messages).

Pick the category honestly. Promotional content filed as Utility is usually rejected or moved to Marketing. Meta can also recategorise an approved template (for example Utility to Marketing), which changes what each message costs. The dashboard flags recategorised templates, and the API always returns Meta’s current category.

Template types

When you create a template you choose a type:

TypeWhat it is
StandardHeader, body, footer and buttons
Limited-time offerA marketing offer with an optional countdown to the time it ends. Needs a copy-code button and a URL button, and can’t have a footer.
AuthenticationA one-time-password template. Meta writes the body text (“123456 is your verification code”), with an optional security warning, code expiry and a copy-code button.
Carousel2 to 10 cards that scroll sideways, each with its own image or video, text and buttons (see below)
  • 2 to 10 cards, each with its own image or video. All cards use the same media type.
  • Optional card text, up to 160 characters per card.
  • 1 or 2 buttons per card: quick reply, link (URL) or phone number. Every card has the same button layout, in the same order.
  • A message body sits above the cards. There is no header above the cards and no footer: each card’s media is its header.
  • Carousels are Marketing templates.

Components and limits

ComponentRules
Header (optional)Text up to 60 characters, or media: image, video or document. Location headers can be created too. Media headers need a sample file for Meta’s review.
Body (required)Up to 1,024 characters. Variables are positional ({{1}}, {{2}}) or named ({{first_name}}). Meta needs a sample value for every variable.
Footer (optional)Up to 60 characters
Buttons (optional)Up to 10 in total, at most 2 URL buttons and 1 phone-number button. Button labels up to 25 characters. Keep quick-reply buttons together.

A template’s variable style is fixed once it is created: Meta keeps the template either named ({{first_name}}) or numbered ({{1}}), so an edit can’t switch between them. Duplicate the template to change style. A template that Meta registered without its variables can’t be sent (422 TEMPLATE_VARIABLES_NOT_REGISTERED); duplicate it to get a working copy.

The template’s name (its Meta ID) uses lowercase letters, numbers and underscores, and like the language it can’t be changed after creation. The name your team sees in the dashboard is a separate display name. Over the API you always send the Meta name.

Button types

ButtonNotes
Quick replyA custom reply the customer taps; arrives as reply.kind: "quick_reply" on message.received
Visit website (URL)Static link, or a dynamic link whose end is filled at send time. Only one URL button per template may have a variable.
Call phone numberOne per template
Call on WhatsApp / video callCalling must be turned on in WhatsApp Manager
Copy offer codeCoupon code up to 15 characters when sent (WhatsApp’s limit); Marketing templates only
Complete flowOpens a WhatsApp Form (Meta Flow); answers arrive in form
Request contact info, catalog, multi-productUse your WhatsApp profile or product catalog in Meta

Review and statuses

Meta reviews every template, and review time is set by Meta. The template’s status tells you whether it can be sent:

StatusMeaning
PENDINGIn review with Meta. Can’t be sent yet.
APPROVEDCan be sent
REJECTEDMeta rejected it; rejectionReason says why. Fix and resubmit in the dashboard.
PAUSEDPaused by Meta for low quality. It can return to Approved on its own.
DISABLEDDisabled by Meta. It won’t come back.

Only APPROVED templates can be sent. Any other status answers 422 TEMPLATE_NOT_APPROVED with details.status. Editing an approved template in the dashboard sends it back to review (PENDING), and it can’t be sent until Meta approves it again. Duplicate it instead if you need the original to keep working.

List templates

GET /api/v1/templates lists your templates, newest first. Needs the templates:read scope. It is a paginated list (see Pagination & Idempotency).

Query parameterDescription
statusAPPROVED, PENDING, REJECTED, PAUSED or DISABLED
categoryMARKETING, UTILITY or AUTHENTICATION
languageExact language code, for example en_US or hi
nameExact template name. The quick way to check a template is approved before you send it.
limit1 to 100, default 25
cursorThe nextCursor from the previous page
curl "https://waba-api.startmessaging.com/api/v1/templates?status=APPROVED&name=order_confirmation" \
  -H "Authorization: Bearer sm_live_your_api_key"
{
  "data": {
    "items": [
      {
        "id": "7d2c4b1a-3e5f-4a6b-9c8d-0e1f2a3b4c5d",
        "name": "order_confirmation",
        "displayName": "Order confirmation",
        "language": "en_US",
        "category": "UTILITY",
        "status": "APPROVED",
        "rejectionReason": null,
        "components": [
          { "type": "BODY", "text": "Hi {{1}}, your order {{2}} is confirmed." }
        ],
        "createdAt": "2026-09-20T06:30:00.000Z",
        "updatedAt": "2026-09-21T09:00:00.000Z"
      }
    ],
    "nextCursor": null
  },
  "meta": { "requestId": "…", "timestamp": "2026-10-02T10:00:00.000Z" }
}
FieldDescription
nameThe name WhatsApp knows the template by. Send it as template.name. It never changes.
displayNameWhat your team renamed it to in the dashboard; never sent to WhatsApp. null if never renamed.
languageLanguage code. A name exists once per language, so send the pair.
categoryMeta’s current category, which can differ from the one submitted
statusMeta’s current review status
rejectionReasonMeta’s reason when the template was rejected
componentsThe template’s parts in Meta’s own format (HEADER, BODY, FOOTER, BUTTONS, and on offer and carousel templates LIMITED_TIME_OFFER and CAROUSEL). The placeholders are what parameters fill when you send it.
updatedAtWhen it last changed, including a review decision from Meta

Get a template

GET /api/v1/templates/{id} returns one template in the same shape. Needs templates:read. An unknown id (or one from another workspace) answers 404 TEMPLATE_NOT_FOUND.

curl https://waba-api.startmessaging.com/api/v1/templates/7d2c4b1a-3e5f-4a6b-9c8d-0e1f2a3b4c5d \
  -H "Authorization: Bearer sm_live_your_api_key"

Templates can’t be created, edited or deleted through the API. Use the dashboard, which shows a live phone preview, needs a sample for every variable and explains rejection reasons.

Status changes: template.status_updated

Subscribe to the template.status_updated webhook (needs templates:read) instead of polling. It fires when Meta approves, rejects, pauses or disables a template, and when an edit in the dashboard sends a template back to review (status: "PENDING").

{
  "id": "evt_…",
  "type": "template.status_updated",
  "createdAt": "2026-10-02T10:00:00.000Z",
  "workspaceId": "6f1e2d3c-4b5a-4968-8776-5a4b3c2d1e0f",
  "data": {
    "template": {
      "id": "7d2c4b1a-3e5f-4a6b-9c8d-0e1f2a3b4c5d",
      "name": "order_confirmation",
      "language": "en_US",
      "category": "UTILITY",
      "status": "APPROVED",
      "previousStatus": "PENDING",
      "rejectionReason": null
    }
  }
}

Sending templates

Send an approved template with POST /api/v1/messages and type: "template". The fields are described on Send Messages:

  • parameters fill the body variables, in order or by name.
  • headerMediaUrl is required when the header is an image, video or document.
  • buttonParameters give send-time values to dynamic buttons: a URL variable (subType: "url"), a coupon code (subType: "copy_code", couponCode up to 15 characters) or a WhatsApp Form flow token (subType: "flow").
  • carouselCards is required for a carousel: one entry per card, in order, each with its headerMediaUrl and any card parameters and link buttonParameters.
  • offerExpiresAt is required for a limited-time offer that shows when it ends: an ISO 8601 date-time in the future.

Each template in the dashboard has a Send via API panel that shows exactly what that template needs and a request you can copy.

A carousel always sends every card it was created with. Give each card its own image or video, plus its text variables and link suffix if it has them:

{
  "to": "+919876543210",
  "type": "template",
  "template": {
    "name": "diwali_collection",
    "language": "en_US",
    "parameters": [{ "text": "Aarti" }],
    "carouselCards": [
      {
        "headerMediaUrl": "https://cdn.example.com/products/kurta-blue.jpg",
        "parameters": [{ "text": "₹1,299" }],
        "buttonParameters": [{ "index": 0, "subType": "url", "text": "kurta-blue" }]
      },
      {
        "headerMediaUrl": "https://cdn.example.com/products/saree-red.jpg",
        "parameters": [{ "text": "₹2,499" }],
        "buttonParameters": [{ "index": 0, "subType": "url", "text": "saree-red" }]
      }
    ]
  }
}

A missing card, image or value answers 422 TEMPLATE_CAROUSEL_CARDS_INCOMPLETE, with details.problems naming each card and what it lacks. Full cURL, Node.js and Python examples are on Send Messages.

Sending a limited-time offer

If the offer template shows a countdown, send the offer’s end time with every message:

{
  "to": "+919876543210",
  "type": "template",
  "template": {
    "name": "diwali_flash_sale",
    "language": "en_US",
    "parameters": [{ "text": "Aarti" }],
    "headerMediaUrl": "https://cdn.example.com/diwali-sale.jpg",
    "offerExpiresAt": "2026-10-20T18:30:00Z",
    "buttonParameters": [
      { "index": 0, "subType": "copy_code", "couponCode": "DIWALI25" },
      { "index": 1, "subType": "url", "text": "diwali" }
    ]
  }
}

Without offerExpiresAt the API answers 422 TEMPLATE_OFFER_EXPIRY_REQUIRED; with a time that has passed, 422 TEMPLATE_OFFER_EXPIRY_INVALID. See Send Messages.

What campaigns can’t send

Campaigns can’t send carousel, limited-time offer, authentication, catalog or call-permission templates; the campaign template picker hides them. They also refuse a template with a location header, or with any button that needs its own value on every send other than a link variable or a coupon code: OTP, catalog, multi-product, payment request, postback and booking status buttons. Carousel, limited-time offer and authentication templates can go out one message at a time, through POST /api/v1/messages or from the inbox.

What the API can’t send yet

  • Location headers. The API has no field for the location, so WhatsApp would refuse the message.
  • Text headers with a variable. The API can’t fill a header variable yet. Use a template without a header variable.

WhatsApp OTP with an authentication template

You can deliver one-time passwords over WhatsApp by sending an Authentication template through POST /api/v1/messages. There is no separate OTP or verify endpoint:

  • You generate the code and verify it yourself. StartMessaging only delivers the message.
  • There is no SMS fallback. If the customer is not on WhatsApp the send fails (for example 422 MESSAGE_UNDELIVERABLE); fall back to your own SMS provider if you need one.

Where the code goes

Send the code once, as the first entry of parameters. That is enough: it fills the body, and StartMessaging puts the same code on the template’s code button for you. You can also send it in buttonParameters at index 0 with subType: "url" (that value is then used for the button); sending it in both places, as below, is harmless. With the code in neither, the API answers 422 TEMPLATE_BUTTON_PARAM_REQUIRED.

{
  "to": "+919876543210",
  "type": "template",
  "template": {
    "name": "login_otp",
    "language": "en_US",
    "parameters": [{ "text": "482913" }],
    "buttonParameters": [{ "index": 0, "subType": "url", "text": "482913" }]
  },
  "clientReference": "login-attempt-7f3a2c"
}

clientReference lets the message.delivered and message.failed webhooks name the login attempt, so you can show “code sent” or offer another channel.

Example (Node.js)

import { randomInt, createHash } from 'node:crypto';

export async function sendWhatsAppOtp(phone, attemptId) {
  const code = String(randomInt(0, 1_000_000)).padStart(6, '0');

  // Store only a hash of the code, with an expiry that matches your template.
  await db.otpAttempts.insert({
    id: attemptId,
    phone,
    codeHash: createHash('sha256').update(code).digest('hex'),
    expiresAt: new Date(Date.now() + 10 * 60 * 1000),
    attempts: 0,
  });

  const res = await fetch('https://waba-api.startmessaging.com/api/v1/messages', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.SM_API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `otp-${attemptId}`,
    },
    body: JSON.stringify({
      to: phone,
      type: 'template',
      template: {
        name: 'login_otp',
        language: 'en_US',
        parameters: [{ text: code }],
        buttonParameters: [{ index: 0, subType: 'url', text: code }],
      },
      clientReference: `login-attempt-${attemptId}`,
    }),
  });

  if (!res.ok) {
    const { error } = await res.json();
    throw new Error(`${error.code}: ${error.message}`);
  }
}

Example (Python)

import os, secrets, requests

def send_whatsapp_otp(phone: str, attempt_id: str) -> str:
    code = f"{secrets.randbelow(1_000_000):06d}"
    # Store a hash of the code and an expiry before sending.
    res = requests.post(
        "https://waba-api.startmessaging.com/api/v1/messages",
        headers={
            "Authorization": f"Bearer {os.environ['SM_API_KEY']}",
            "Idempotency-Key": f"otp-{attempt_id}",
        },
        json={
            "to": phone,
            "type": "template",
            "template": {
                "name": "login_otp",
                "language": "en_US",
                "parameters": [{"text": code}],
                "buttonParameters": [{"index": 0, "subType": "url", "text": code}],
            },
            "clientReference": f"login-attempt-{attempt_id}",
        },
    )
    res.raise_for_status()
    return code

Good practice for your own verification step: compare hashes in constant time, expire codes after a few minutes, limit attempts per code and resends per number, and invalidate the code once it is used.

FAQ

Can I create or submit templates through the API? No. Templates are created and submitted in the dashboard, where Meta’s sample-value and media rules are checked before submission. The API lists templates and sends approved ones.

How long does Meta take to review a template? Meta sets the review time. The template stays PENDING until Meta decides; subscribe to template.status_updated to know the moment it changes.

My send failed with TEMPLATE_NOT_FOUND. Why? Check that you sent the Meta name (not the display name) and the exact language code. The same name in another language is a different template.

Does StartMessaging have an OTP or verify API for WhatsApp? No. Send an Authentication template with POST /api/v1/messages, putting the code in parameters (repeating it in buttonParameters is optional). Generating, storing and verifying the code is up to your system, and there is no automatic SMS fallback.

Can I send carousel templates through the API? Yes. Send one carouselCards entry per card, in the template’s order, each with its own headerMediaUrl. Campaigns can’t send carousels, so the API and the inbox are the ways to send them.

Are utility templates still free inside the 24-hour window? No. Since 1 October 2026 Meta charges every delivered template, utility included, inside the window too. Service replies are free up to a monthly allowance per phone number.

Why did Meta change my template’s category? Meta can move a template between Utility and Marketing after approval when its content looks promotional. This changes its price. The dashboard flags it, and the API returns the current category.