Template Definition

A template definition is the JSON document that describes a template's content. It is the required definition field of the Create Template (POST /v3/templates) and Update Template (PUT /v3/templates/{id}) requests. The dashboard template builder displays the same JSON via its View JSON action.

Top-level request fields use snake_case (submit_for_review); field names inside definition use camelCase (multiChannel, variableType).

Create template request

FieldTypeRequiredDescription
categorystringNoMARKETING, UTILITY, or AUTHENTICATION. Auto-detected from the template content when omitted
languagestringNoLanguage code (for example, en_US). Auto-detected from the template content when omitted
definitionobjectYesThe definition object
creation_sourcestringNoSource label for the template. Default: from-api
submit_for_reviewbooleanNotrue submits the template for review immediately after creation. Default: false, which saves the template with status DRAFT
sandboxbooleanNotrue validates the request and returns a simulated template without creating anything. Default: false. See Sandbox mode

There is no name field on create: the display name is derived from the template's content. To rename a template afterwards, send name on Update Template (PUT /v3/templates/{id}).

Request headers

Beyond the x-api-key header that authenticates every call, the template endpoints accept two optional headers:

HeaderAccepted onPurpose
Idempotency-KeyPOST /v3/templates, PUT /v3/templates/{id}Makes a retry safe: the first response is cached for 24 hours per key per customer and replayed instead of creating a second template. 1–255 characters matching ^[a-zA-Z0-9_-]+$. See Idempotency
x-profile-idEvery template endpointScopes the request to a child profile, so the template is created, listed, or changed under that profile rather than the key's own account. Organization API keys only, and the profile must belong to the calling organization. See Multi-tenant architectures

GET and DELETE ignore Idempotency-Key, which is why it is not offered on them. A profile-scoped request echoes the profile back in the X-Profile-Id response header, and the template's owner is reported as customer_id on the template itself.

Definition object

FieldTypeRequiredDescription
headerobjectNoHeader object
bodyobjectYesBody object
footerobjectNoFooter object
buttonsarrayNoArray of button objects
definitionVersionstringNoVersion of the template definition format
authenticationConfigobjectNoAuthentication configuration, used only by AUTHENTICATION templates

Channel support

ComponentSMSWhatsAppRCSMMS
HeaderNot supportedNative headerPrepended to message textNot supported; MMS has its own subject instead
BodySupportedSupportedSupportedSupported, from the mms body only
FooterNot supportedNative footerAppended to message textNot supported
ButtonsNot supportedInteractive buttonsFirst four shown as suggestion chipsNot supported

MMS is the one channel that renders no header, footer, or buttons at all: it is a carrier message carrying a subject, text, and attachments, with nowhere to put them.

Header object

FieldTypeRequiredDescription
typestringNoHeader type: text, image, video, or document
templatestringYesHeader text with optional variable placeholders. Maximum 60 characters
variablesarrayNoVariable objects used in the header. Maximum 1

Text headers must not contain newlines, emojis, or formatting markup (*, _, ~). AUTHENTICATION templates do not use headers; Sent excludes any header from them automatically.

Body object

The body carries the message copy. Pick one of two authoring strategies:

  • a shared multiChannel body on its own, or
  • an explicit sms + whatsapp pair, with both present.

rcs and mms are optional on top of either strategy.

FieldTypeRequiredDescription
multiChannelobjectConditionalThe body used for every channel. Required unless you send both sms and whatsapp
smsobjectConditionalThe SMS body. Must be paired with whatsapp, and cannot be combined with multiChannel
whatsappobjectConditionalThe WhatsApp body. Must be paired with sms, and cannot be combined with multiChannel
rcsobjectNoRCS-specific copy that overrides the chosen strategy for RCS only. Cannot be the only body present
mmsobjectNoMMS body content object: the subject, copy, and attachments MMS sends. Cannot be the only body present, and its absence makes the template not MMS-capable

The channel bodies are a strategy, not overrides

sms and whatsapp do not override multiChannel, they replace it. The API refuses a body that mixes the two strategies (multiChannel alongside sms or whatsapp), and it equally refuses sms on its own, whatsapp on its own, or rcs on its own: every template is expected to be deliverable on every channel. A body that breaks the rule fails with "Body must have either multiChannel OR both sms and whatsapp channels (not both strategies). An rcs or mms body may accompany either, but cannot stand on its own."

Body content object

FieldTypeRequiredDescription
typestringNoThe body block's type: send text
templatestringYesBody text with variable placeholders. Maximum 1024 characters, or 3072 in an rcs body and 1600 in an mms body, and subject to the body content rules
variablesarrayNoVariable objects used in the body, one entry per {{index:variable}} placeholder. IDs must be unique within the body

Always send the body type

type is schema-optional, but a body posted without it is stored with no type key at all, and the dashboard template builder has nothing to render the block from. Send "type": "text" on every body content object.

MMS body content object

The mms body is the body content object plus the two members only MMS has: a subject line and attachments.

FieldTypeRequiredDescription
typestringNoThe body block's type: send text
templatestringConditionalBody text with variable placeholders. Maximum 1600 characters, a wider cap than the other channels because an MMS text part is not bound by Meta's limit. Optional when media carries at least one item
variablesarrayNoVariable objects used in template and in subject
subjectstringNoSubject line, maximum 80 characters, rendered above the body on most handsets and ignored by some. Variables are substituted, and the result is truncated rather than rejected if it overruns
mediaarrayConditionalOne MMS media item. MMS carries one attachment, so a second is rejected at authoring time. Optional when template carries text

An mms body must carry body text, at least one media item, or both. One that carries neither renders to nothing and is rejected at authoring time with "MMS body must carry body text, at least one media item, or both." A template saved with more than one media item before the one-attachment limit still sends, carrying only its first.

MMS never falls back to another body

Every other channel resolves to a shared body when it has no dedicated one: rcs falls back to multiChannel and then to sms. MMS resolves to the mms body and nothing else. A template without one is not MMS-capable, produces no MMS route candidate, and cannot be sent with "channel": ["mms"]. This is deliberate: MMS with no media is a more expensive SMS.

MMS media item

FieldTypeRequiredDescription
urlstringYesPublicly fetchable https URL on a hostname. A literal IP address is refused. The carrier fetches it at send time, including on retries, so it must stay reachable and unauthenticated for the life of the send. A presigned URL is not a valid value. Sent reads the size it declares before each send, and an attachment over 600 KB is not sent as MMS (see Media Size Limits)
mediaTypestringNoOne of image, video, document, or contact. Any other value, including audio, is rejected. Advisory only: the carrier reads the Content-Type of the fetched object, not this field. It exists so an authoring UI can preview the attachment and a reviewer can see what was intended

Media URLs are never templated. A variable is substituted in template and subject but copied verbatim in url, so an attachment cannot be assembled from caller input.

A per-send media_urls on POST /v3/messages replaces this list rather than adding to it, which lets a template hold a default creative while a caller sends something recipient-specific. See Sending MMS for the sending side.

FieldTypeRequiredDescription
typestringNoFooter type: text
templatestringYesFooter text. Maximum 60 characters
variablesarrayNoMust be empty; footers cannot contain variables

Footers must not contain variables, newlines, emojis, or formatting markup (*, _, ~).

Buttons array

A template holds a maximum of 10 buttons. Button IDs must be unique. Per-type limits: maximum 1 COPY_CODE, 1 PHONE_NUMBER, 2 URL, and 10 QUICK_REPLY buttons.

FieldTypeRequiredDescription
idnumberNo*Button identifier (1-based index), unique within the template. *Required in practice once a template has two or more buttons, see below
typestringYesQUICK_REPLY, URL, VOICE_CALL, PHONE_NUMBER, or COPY_CODE
propsobjectYesType-specific button properties

Button props object

FieldTypeApplies toDescription
textstringAll typesButton label. Maximum 25 characters, static text only (see below)
quickReplyTypestringQUICK_REPLYcustom or pre-configured
urlTypestringURLstatic or dynamic
urlstringURLDestination URL. Maximum 2000 characters
variablesarrayURL (dynamic)Exactly 1 variable object; its placeholder must appear at the end of url
activeFornumberVOICE_CALLInteger greater than 0
countryCodestringPHONE_NUMBERCountry code (for example, US)
phoneNumberstringPHONE_NUMBERPhone number in international format
offerCodestringCOPY_CODECode copied to the clipboard on tap
otpTypestringAUTHENTICATION OTP buttonsCOPY_CODE or ONE_TAP

Button labels take static text only

Meta allows no dynamic content in a button label, so text is refused when it contains a {{...}} variable placeholder, a newline, an emoji, or WhatsApp formatting markup (*, _, ~). Meta reports all four as a single error ("Buttons can't have any variables, newlines, emojis, or formatting characters."), so Sent checks them at save time instead, one message per rule. Put the dynamic part in the body, or in a dynamic URL button's url, where placeholders are allowed.

Number your buttons when there are two or more

id is schema-optional, but it is a plain integer rather than a nullable one: a button that omits it is stored with id: 0. One button that way is fine, two are not: they collide, and the template is refused with "Button IDs must be unique." Number them from 1 in the order you want them shown, which also decides which four reach RCS.

Variable object

Variables use a numbered placeholder syntax in template text: {{0:variable}} for text variables and {{4:link}} for links, where the number is the variable's id and the word after the colon is its type. This is the canonical form, and the one the dashboard builder writes.

Two shorter forms are also accepted in header and body text, and resolve to the same variable:

FormExampleNotes
{{id:type}}{{0:variable}}Canonical. Use this form
{{name}}{{customerName}}Matched against the variable's name
{{id}}{{0}}Matched against the variable's id

Dynamic URL buttons take the numeric form only

A dynamic URL button is validated with a numeric-only pattern, so its url must end in {{0:variable}} or {{0}}. A named placeholder such as https://example.com/orders/{{orderNumber}} fails with "Dynamic URL must end with a variable placeholder (e.g. {{0:variable}})."

FieldTypeRequiredDescription
idnumberNo*Sequential ID starting from 0, unique within its section. *Required in practice once a section has two or more variables, and it is the number the {{0:variable}} placeholder refers to
namestringYesReadable identifier for the variable (for example, orderNumber)
typestringYesvariable, link, or media
propsobjectYesVariable properties

Placeholder types are validated at save time

The type token in a {{index:type}} placeholder must be variable, link, or media. Any other value is rejected when the template is saved:

"template contains a placeholder with an unsupported type. Placeholders must use 'variable', 'link', or 'media' (e.g. {{0:variable}})."

Before this validation was enforced, an unrecognized type such as {{0:message}} was stored without error. At send time the placeholder could not be resolved, so the literal string was delivered to the recipient instead of the substituted value.

Variable props object

FieldTypeApplies toDescription
variableTypestringvariableRequired. text, link, image, or file
samplestringvariableRequired. Example value shown in previews and used during review
regexstringvariableOptional validation pattern for the variable value
urlstringlink, mediaRequired. Full HTTP or HTTPS URL
shortUrlstringlinkOptional shortened URL
altstringlink, mediaAlternative text
mediaTypestringmediaRequired. image, video, or document

Authentication configuration

AUTHENTICATION templates accept an authenticationConfig object:

FieldTypeDescription
addSecurityRecommendationbooleanAdds the text "For your security, do not share this code."
codeExpirationMinutesnumber1–90. When set, adds the footer "This code expires in X minutes."

An AUTHENTICATION body must declare exactly one text variable (the code) and must not contain links or emojis.

Content rules and limits

RuleLimit
Body length1024 characters for the multiChannel, sms and whatsapp bodies; 3072 for an rcs body; 1600 for an mms body
MMS subject length80 characters
MMS media items10 per mms body
Header and footer length60 characters each
Header variables1
Buttons per template10
Button label length25 characters
Button URL length2000 characters
Variables in a dynamic URL buttonExactly 1, at the end of the URL
Emojis in a MARKETING body10 (dashboard only, see below)
Placeholder typesvariable, link, or media. Any other type is refused at save time

The emoji cap is checked on dashboard creation only

The 10-emoji cap on MARKETING bodies is applied when a template is first created in the dashboard. The v3 API does not check it, so POST /v3/templates and PUT /v3/templates/{id} both accept a MARKETING body carrying more, and no update path re-checks it. Treat it as the limit to design to rather than a validation you can rely on.

Length limits are validated at save time against the stored text, with {{...}} placeholders unsubstituted. Counting is encoding-blind: each UTF-16 code unit counts as one character (the behavior of .NET's string.Length and JavaScript's String.prototype.length), so characters in the Basic Multilingual Plane count as 1 and most emoji count as 2. There is no post-substitution validation; a body within the limit can render into a longer message once variables expand. For how rendered length maps to SMS segments, see SMS Encoding & Message Length.

Meta requires every variable to carry surrounding context, so body text must additionally satisfy all of these rules. A body that fails any of them is refused at save time:

  • At least one letter before the first variable and after the last. Trailing punctuation does not count: ... {{1:variable}}. fails the rule.
  • At least (2 × variable count) + 1 words once the placeholders are removed.
  • No two variables adjacent with only whitespace between them.
  • The text cannot begin or end with a newline, run more than two line breaks together, or run more than four spaces together.

For example, Hello {{0:variable}}! Welcome to {{1:variable}}. We are glad to have you on board. passes: it has two variables, so at least five words are required, and the copy after the final variable contains letters.

AUTHENTICATION bodies are exempt from these rules and are held to Meta's authentication format instead.

Template statuses

StatusMeaning
DRAFTCreated but not submitted. Editable and testable; cannot be sent on any channel
PENDINGSubmitted for review; awaiting a decision
APPROVEDApproved; available for sending
REJECTEDDeclined by the reviewer. A rejected template must be revised and resubmitted before it can send
PAUSEDPaused by Meta for repeated low quality. The template stays published, but sends against it are blocked until it is reinstated
DISABLEDDisabled by Meta, typically for a policy violation. Treated as a takedown: nothing sends
REVOKEDTaken down on a channel by Sent or Meta. Nothing sends on that channel until it is reinstated

Review routing depends on your account:

  • With a connected WhatsApp Business Account, Meta reviews the template. A Meta APPROVED or PENDING verdict applies to every channel (SMS, WhatsApp, and RCS); a REJECTED verdict applies to the WhatsApp channel only.
  • Without a connected WhatsApp Business Account, Sent's compliance team reviews the template, and each channel is approved or rejected individually.

Approval is tracked per channel. A message sends on a channel only when the template is approved for that channel; sends against unapproved templates return status BLOCKED. Status changes are delivered as templates webhook events carrying the template ID, status, reason, and the channel leg the decision applies to. Each channel is reviewed independently and reports separately, so expect one event per leg. channel is omitted when the decision applies to the template as a whole rather than to one leg; that event is the broader news, because a template-wide rejection blocks every channel whatever the individual legs say.

What a status allows you to change

Update Template (PUT /v3/templates/{id}) treats name differently from the rest of the template. The display name is editable in every status. The content fields and resubmission are gated:

Statusnamedefinition, category, languagesubmit_for_review
DRAFTEditableEditableSubmits for review
REJECTEDEditableEditableResubmits for review
APPROVEDEditableEditable as a live editRe-opens review
PENDINGEditableFrozen: 409 CONFLICT_006Frozen: 409 CONFLICT_006
PAUSED, DISABLED, REVOKEDEditableRefused: 400 VALIDATION_001Accepted, but does nothing

A template is also treated as in review, and so frozen, when its own status is not PENDING but any of its channels is still awaiting a verdict. The 409 carries the message "This template is in review; its definition, category and language are frozen until the review completes".

From PAUSED, DISABLED, or REVOKED, an edit to definition, category, or language is refused with a 400 whose details.request reads "Template (except display name) cannot be updated unless it is in draft or rejected status". To change the content of a paused or taken-down template, create a new one.

Resubmitting a paused or taken-down template is a no-op

submit_for_review: true on a template in PAUSED, DISABLED, or REVOKED is accepted and answers 200, but no review is opened and the status does not move. Only the reviewer can reinstate the template, so treat the 200 as a stored request rather than a resubmission, and confirm through the templates webhook or a follow-up GET.

Editing an approved template

An approved template can be edited in place; this is a live edit. The new content is stored immediately, and sending submit_for_review: true re-opens review, which returns the affected channels to PENDING. Those channels stop sending until they are approved again, and the previously approved content is not sent in the meantime. Plan for a gap, or keep a second approved template to send from while the edit is in review.

How much of the template goes back to PENDING depends on how your account is reviewed, so watch the per-channel templates webhook events rather than reading the template-level status alone.

Sent-provisioned templates are read-only

Templates that Sent provisions into your account (the pre-built OTP and verification templates that come with light onboarding) are read-only. They accept only submit_for_review; any other field is refused with 400 VALIDATION_001 and the detail "This template is read-only. Only 'submit for review' is allowed." To send different content, create your own template.

Relatedly, the sent_ prefix is reserved for these templates, so a name beginning with sent_ is refused on a template of your own. Pick any other name.

Example

A complete POST /v3/templates request body with a header, a multi-channel body using text variables and a dynamic link, a footer, and two buttons:

{
  "category": "UTILITY",
  "language": "en_US",
  "submit_for_review": false,
  "definition": {
    "header": {
      "type": "text",
      "template": "Order {{0:variable}}: Delivery Update",
      "variables": [
        {
          "id": 0,
          "name": "orderNumber",
          "type": "variable",
          "props": {
            "variableType": "text",
            "sample": "12345"
          }
        }
      ]
    },
    "body": {
      "multiChannel": {
        "type": "text",
        "template": "Hi {{0:variable}},\nYour Acme order {{1:variable}} has been shipped and is expected to arrive {{2:variable}} between {{3:variable}}.\nYou can track your package or update your delivery preferences at {{4:link}} before the driver arrives.",
        "variables": [
          {
            "id": 0,
            "name": "customerName",
            "type": "variable",
            "props": {
              "variableType": "text",
              "sample": "Lucas"
            }
          },
          {
            "id": 1,
            "name": "orderNumber",
            "type": "variable",
            "props": {
              "variableType": "text",
              "sample": "#12345"
            }
          },
          {
            "id": 2,
            "name": "arrivalDate",
            "type": "variable",
            "props": {
              "variableType": "text",
              "sample": "tomorrow (Oct 11)"
            }
          },
          {
            "id": 3,
            "name": "arrivalTime",
            "type": "variable",
            "props": {
              "variableType": "text",
              "sample": "2 PM – 4 PM"
            }
          },
          {
            "id": 4,
            "name": "orderLink",
            "type": "link",
            "props": {
              "url": "https://example.com",
              "shortUrl": "",
              "alt": "Tracking Page"
            }
          }
        ]
      }
    },
    "footer": {
      "type": "text",
      "template": "Thank you for shopping with Acme.",
      "variables": []
    },
    "buttons": [
      {
        "id": 1,
        "type": "URL",
        "props": {
          "text": "Track your order",
          "urlType": "static",
          "url": "https://www.example.com/track"
        }
      },
      {
        "id": 2,
        "type": "PHONE_NUMBER",
        "props": {
          "text": "Acme customer support",
          "countryCode": "US",
          "phoneNumber": "+112345678"
        }
      }
    ]
  }
}

The endpoint responds with the standard response envelope; the created template's fields are documented on the Create Template endpoint page.

On this page