Message templates
Author WhatsApp message templates in TSX, publish them, submit them to Meta for review, and send them once approved.
Outside a recipient's 24-hour service window, every WhatsApp message must be a Meta-approved template: fixed text with placeholders you fill at send time. A WhatsApp template in Samva is a template like any other. You write it in TSX, publish it, then submit it to Meta for review. Once Meta approves it, you send it by its handle with the values its placeholders need.
Write a template
A WhatsApp template declares its input schema, at least one fixture, and a whatsapp message built
from Meta's template anatomy: a body, and optionally a header, a footer, and buttons. Start from one
of the WhatsApp starters on the dashboard's Templates page, or write the entry yourself:
/** @jsxImportSource @samva/markup/email */
import { defineTemplate } from "@samva/markup";
import { jsonSchema } from "@samva/markup/input-schema";
import { WhatsApp } from "@samva/markup/whatsapp";
export default defineTemplate({
id: "order-update",
schema: jsonSchema<{ firstName: string; orderNumber: string }>({
type: "object",
properties: { firstName: { type: "string" }, orderNumber: { type: "string" } },
required: ["firstName", "orderNumber"],
additionalProperties: false,
}),
fixtures: { default: { firstName: "Maya", orderNumber: "NW-10482" } },
whatsapp: (input) => (
<WhatsApp language="en_US" category="utility">
<WhatsApp.Body>
{"Hi "}
{input.firstName}
{", your order "}
{input.orderNumber}
{" has shipped."}
</WhatsApp.Body>
<WhatsApp.Footer>{"Reply STOP to unsubscribe"}</WhatsApp.Footer>
<WhatsApp.Buttons>
<WhatsApp.UrlButton url={`https://example.com/track/${input.orderNumber}`}>
{"Track order"}
</WhatsApp.UrlButton>
</WhatsApp.Buttons>
</WhatsApp>
),
});Each value you read from input becomes a numbered placeholder in the template Meta reviews, in the
order it first appears: the body above registers as Hi {{1}}, your order {{2}} has shipped. The
template's first fixture supplies the example values Meta asks for.
Meta approves the text, so the shape of the message is fixed:
- Text parts hold literal text and values only, with no conditions or loops.
- The body begins and ends with fixed words, not a value. Punctuation around a value does not count:
Hello {{1}}.needs a closing phrase, such asHello {{1}}. Your order has shipped. - A header carries at most one value; a footer carries none.
- Buttons are fixed: no button that appears on a condition, and labels are literal text.
- A URL button's address starts with a fixed
httpsURL and may vary only at its end. - A template has at most one flow button, which opens a flow.
- Headers are text. Media headers and authentication templates are not supported.
Publishing refuses a template that breaks one of these rules and lists every reason.
Open a flow from a button
WhatsApp.FlowButton is a button that opens one of your WhatsApp flows
when the customer taps it. This is how a flow opens outside the 24-hour service window, because a
template can start a conversation and a flow message cannot.
<WhatsApp.Buttons>
<WhatsApp.FlowButton flow="appointment" screen="DETAILS">
{"Book now"}
</WhatsApp.FlowButton>
</WhatsApp.Buttons>| Prop | Meaning |
|---|---|
flow | The flow's handle, such as appointment for flows/appointment.json. Lowercase letters, numbers, and hyphens. |
action | Optional. navigate (the default) opens a screen of the flow; data_exchange asks the flow's endpoint for the first screen. |
screen | Optional. The screen a navigate button opens on, instead of the flow's first entry screen. Refused with data_exchange. |
| children | The button label, fixed text. |
The template names the flow by its handle; Samva swaps in the flow's id on Meta when it submits the
template, because Meta numbers the same flow differently on each business account. So the flow must
be registered on that account before you submit the template: create the flow, publish a
version, and register it (see WhatsApp flows).
A submission is refused with a validation error that says what is missing when the handle names no
flow of yours, or when the flow has no published or throttled registration on the template's
business account.
The template opens the flow version it was submitted with. Publishing and registering a newer version of the flow does not change a template Meta has already approved; submit the template again to open the newest published version.
Name, language, and category
Meta's name for the template comes from its handle, with hyphens as underscores: the handle
order-update registers as order_update. Set language to a Meta language code such as en_US;
a template has one language. A business account holds one template per name and language, so
submitting a second template with the same name and language is refused; give it its own handle.
category is the category you declare, utility or marketing:
| Category | Use for |
|---|---|
utility | Order updates, receipts, account notifications. |
marketing | Promotions, offers, announcements. |
Meta can assign a different category when it reviews the template. A send is billed on the category Meta assigns.
To change the category of a rejected template, publish the new category and submit again. Meta does not change the category of an approved template, so that submission is refused; change the handle to register the content as a new template under the new category.
Submit for review
Publishing a template does not send it to Meta. Submit it when you're ready: from the Message templates section of the dashboard's WhatsApp page, or with the API:
curl -X POST https://api.samva.dev/v1/templates/tmpl_7q2xk9mvt4znw8rh/registrations \
-H "X-API-Key: samva_sk_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "provider": "whatsapp" }'The template registers on the business account of your default number. Pass businessAccountId to
register it on another connected number's account. The response is the registration:
{
"id": "treg_3xk9mvt4znw8rh7q",
"templateId": "tmpl_7q2xk9mvt4znw8rh",
"publicationId": "tpub_k9mvt4znw8rh7q2x",
"target": { "provider": "whatsapp", "wabaId": "102290129340398" },
"name": "order_update",
"language": "en_US",
"status": "submitted",
"category": "utility",
"providerCategory": "utility",
"providerTemplateId": "1689556908129832",
"reason": null,
"submittedAt": "2026-10-13T10:30:00.000Z",
"statusChangedAt": "2026-10-13T10:30:00.000Z",
"createdAt": "2026-10-13T10:30:00.000Z",
"updatedAt": "2026-10-13T10:30:00.000Z"
}Read a template's registrations with GET /v1/templates/{id}/registrations, or one with
GET /v1/templates/{id}/registrations/{registrationId}.
Review status
Meta reviews the template, and the registration's status follows its decision:
| Status | Meaning |
|---|---|
draft | Recorded, but Meta has not accepted the submission yet. Submit again. |
submitted | Meta is reviewing it. Wait for the decision. |
approved | Meta approved it. It sends. |
rejected | Meta rejected it; reason says why. Fix the template and submit again. |
paused | Meta paused it after low-quality feedback; reason says why. |
disabled | Meta disabled it. It no longer sends and cannot be resubmitted. |
Each change sends a template.registration.updated webhook event with the registration and the
status it moved from.
Change an approved template
Publish the change, then submit again. Samva sends the new content to the same Meta template, and Meta reviews it again. Until Meta approves the new content, the template does not send, so a send never fills new placeholders into old approved text. A message accepted before the resubmission fails instead of sending its values into the new text; send it again once Meta approves.
Send a template
Reference the template by its handle or id and pass the values its schema declares; see Send a message. Samva checks the values against the template's schema and fills each placeholder the way the template renders it. A template with no approved registration on the sending number's business account is refused with a conflict error.
A template with a flow button takes an optional flowToken beside
templateData. The flow returns it unchanged as flowToken on the customer's answer and on every
request to the flow's endpoint. Without one, the button carries the message's id; a broadcast or a
journey step gives each contact's message its own id, so the token tells you whose flow it is.
samva whatsapp send --to +16175559876 --template appointment-invite --flow-token booking-4821{
"channel": "whatsapp",
"to": [{ "phone": "+16175559876" }],
"whatsapp": { "templateSlug": "appointment-invite", "flowToken": "booking-4821" }
}Related documentation
Onboard your customers' WhatsApp numbers
Send each of your customers a setup link that connects their WhatsApp number into their namespace, then react to the connection in a webhook.
Send a WhatsApp message
Send an approved template anytime, or free-form text, media, and interactive messages inside the 24-hour service window.