Every message type with its description, request payload, webhook response, limits, and a field-by-field required / optional breakdown. Companion to the Postman collection.
Set up & secure your callback, then the inbound envelopes (messages, status, template status).
Inbound webhooks arrive as a POST to your configured callback URL. Everything is wrapped in the same outer envelope —
object → entry[] → changes[] → value. Inside value you get either a
messages[] array (a user message) or a statuses[] array (a delivery receipt). Per-message webhook responses
shown elsewhere are the object found inside value.messages[].
When you register a callback URL, Meta sends a one-time GET to prove you own the endpoint. Echo back hub.challenge as plain text only if hub.verify_token matches the token you configured.
GET {{callback_url}}
?hub.mode=subscribe
&hub.verify_token=YOUR_VERIFY_TOKEN
&hub.challenge=1158201444
Every event POST is signed. Verify the X-Hub-Signature-256 header — an HMAC-SHA256 of the raw request body keyed with your App Secret — before trusting any payload. Then subscribe your app (callback + verify token) and subscribe the WABA so events start flowing.
POST {{callback_url}}
Content-Type: application/json
X-Hub-Signature-256: sha256=3f1c0a... (HMAC-SHA256 of the raw body, keyed with APP SECRET)
{ ...event payload (messages / statuses / template status)... }
const crypto = require('crypto');
// IMPORTANT: capture the RAW body, e.g. express.json({ verify:(req,_,buf)=>{ req.rawBody = buf; } })
function verifySignature(req) {
const received = req.get('X-Hub-Signature-256') || '';
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.META_APP_SECRET)
.update(req.rawBody) // raw bytes, NOT the parsed JSON
.digest('hex');
return received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}
# App-level: point the app at your callback + verify token
curl -X POST 'https://graph.facebook.com/v23.0/{{app_id}}/subscriptions' \
-d 'object=whatsapp_business_account' \
-d 'callback_url={{callback_url}}' \
-d 'verify_token={{verify_token}}' \
-d 'fields=messages,message_template_status_update,message_template_quality_update' \
-d 'access_token={{app_id}}|{{app_secret}}'
# Subscribe your app to THIS WhatsApp Business Account's events
curl -X POST 'https://graph.facebook.com/v23.0/{{waba_id}}/subscribed_apps' \
-H 'Authorization: Bearer {{access_token}}'
Fields
Field
Type
Required?
Notes
X-Hub-Signature-256
header
Always
Format "sha256=<hex>"; HMAC-SHA256(raw body, app secret).
object
string
Required
For App subscription: "whatsapp_business_account".
callback_url
string
Required
Your HTTPS endpoint that handles GET + POST.
verify_token
string
Required
Arbitrary secret you choose; echoed in the GET handshake.
fields
csv
Required
Webhook fields to receive, e.g. messages, message_template_status_update.
/subscribed_apps
endpoint
Required
POST per WABA to actually start delivery (Bearer token).
Limits
Constraint
Value
HMAC input
the RAW request body bytes — never the re-serialized JSON
The full envelope every inbound user message arrives in. The messages[] object carries a type field that tells you which sub-object to read (text shown here). A context object is present when the user replied to one of your messages.
Delivery receipts for messages you sent: sent → delivered → read, or failed. Same outer envelope as a message webhook, but value carries statuses[] instead of messages[].
Fired whenever a template's review status changes — approval, rejection, pausing, etc. Arrives under the message_template_status_update field (not messages). Use it to flag templates as sendable or to alert your team on a rejection.
Actions on an incoming message: read receipts, typing, contextual reply.
Message Utility — these act on an incoming message, so you need its wamid from the inbound webhook.
Read receipts and typing indicators use a lightweight status payload (no recipient_type / to / type envelope).
Contextual reply is a normal outbound message with an added context object.
Marks the message read and shows the "typing…" bubble on the user's side while you prepare a reply. Same envelope as a read receipt, plus a typing_indicator object.
# waba_id is required as a query parameter on every call
curl -X POST '{{base_url}}/{{phone_number_id}}/messages?waba_id={{waba_id}}' \
-H 'Authorization: Bearer {{access_token}}' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"status": "read",
"message_id": "{{wamid_incoming}}",
"typing_indicator": {
"type": "text"
}
}'
Quotes a previous message above your reply. Add a context object to any outbound message — the rest of the payload is a normal send of that type (text shown here).
# waba_id is required as a query parameter on every call
curl -X POST '{{base_url}}/{{phone_number_id}}/messages?waba_id={{waba_id}}' \
-H 'Authorization: Bearer {{access_token}}' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "{{recipient_phone}}",
"context": {
"message_id": "{{wamid_to_reply_to}}"
},
"type": "text",
"text": {
"body": "Replying to your earlier message"
}
}'
Build & submit templates for approval via the Message Templates API.
Variables — named & numbered
creation
At creation a template may define its variables in either style — both numbered and named variables are accepted by Meta — but a single template must use one style throughout, and every variable needs an example value so the template can be reviewed.
Numbered variables
Placeholders are {{1}}, {{2}}, {{3}} … numbered sequentially from 1. Examples are a positional array — one value per number, in order.
"body": {
"text": "Hi {{1}}, your order {{2}} is confirmed.",
"example": { "body_text": [["John", "#A1234"]] }
}
Named variables
Placeholders are words like {{customer_name}}, {{order_id}} (lowercase letters, digits and underscores). Each example is given by name, so order does not matter.
"body": {
"text": "Hi {{customer_name}}, your order {{order_id}} is confirmed.",
"example": {
"body_text_named_params": [
{ "param_name": "customer_name", "example": "John" },
{ "param_name": "order_id", "example": "#A1234" }
]
}
}
Variable rules
These rules apply to both numbered and named variables:
A variable can never be the first or the last character of the body — there must be text before it and after it ({{1}} hello and hello {{order_id}} are both rejected).
Two variables may not be adjacent — put fixed text between them ({{1}} {{2}} is rejected).
Every variable must be wrapped in double curly braces{{ }} — {{1}} or {{order_id}}, never single braces or a bare number/word.
A text header allows at most one variable; a footer allows none.
Submit a template for approval. Body = name, language, category, and a components[] array (header, body, footer, buttons). Variables are numbered: {{1}}, {{2}}, … — each one needs an example so reviewers can preview it.
# waba_id is required as a query parameter on every call
curl -X POST '{{base_url}}/{{waba_id}}/message_templates?waba_id={{waba_id}}' \
-H 'Authorization: Bearer {{access_token}}' \
-H 'Content-Type: application/json' \
-d '{
"name": "order_confirmation",
"language": "en_US",
"category": "utility",
"components": [
{
"type": "header",
"format": "text",
"text": "Order {{1}}",
"example": { "header_text": ["#A1234"] }
},
{
"type": "body",
"text": "Hi {{1}}, your order {{2}} is confirmed.",
"example": { "body_text": [["John", "#A1234"]] }
},
{
"type": "footer",
"text": "Atmik Bharat"
},
{
"type": "buttons",
"buttons": [
{ "type": "quick_reply", "text": "Track order" }
]
}
]
}'
header_text / body_text — numbered example values.
Limits
Constraint
Value
Template name
≤ 512 chars; lowercase a–z, 0–9, _ only
Header — text
≤ 60 chars; max 1 variable
Header — media
image / video / document via handle (no id/URL)
Body
≤ 1024 chars; can’t start/end with a variable; no two adjacent variables
Footer
≤ 60 chars; plain text, no variables
Button title
≤ 25 chars (every button type)
URL button
URL ≤ 2000 chars; HTTPS only; max 2
Phone button
max 1 per template
Copy-code value
≤ 15 chars; A–Z and 0–9 only
Quick-reply
title ≤ 25; no id (tap returns the title as payload)
Buttons total
max 10; same-type buttons must be consecutive
Variables
numbered {{1}}, {{2}}…; each needs an example
Endpoint for this whole section: POST {{base_url}}/{{waba_id}}/message_templates (Bearer {{access_token}}). Variables are numbered only — {{1}}, {{2}}, … (named parameters are not used). Component type, header format, and button type are case-insensitive; we use lowercase to match Meta's current docs. Media headers use a handle from the Vertex Suite media wrapper (see Media upload) — never Graph, and never a media id at creation. Approval lands on the Template status webhook.
A template may have one optional header. It can be a single line of text (with at most one variable), a piece of media (image / video / document), a location, or a product (single-product templates). Media is referenced by a handle from the Vertex Suite media wrapper — see Media upload. No media id here.
// TEXT — one short line, max 1 variable
{
"type": "header",
"format": "text",
"text": "Receipt for {{1}}",
"example": { "header_text": ["Order #A1234"] }
}
// MEDIA — image / video / document (use a handle from the media wrapper)
{
"type": "header",
"format": "image",
"example": { "header_handle": ["4:aW1hZ2U...:e:1700000000:..."] }
}
// LOCATION — pin supplied at send time
{ "type": "header", "format": "location" }
// PRODUCT — single-product templates pull the image from the catalog
{ "type": "header", "format": "product" }
Fields
Field
Type
Required?
Notes
format
string
Required
text / image / video / document / location / product.
text
string
format=text
Header line; max 1 variable.
example.header_text
array
If text variable
One example value for the {{1}} in the header.
example.header_handle
array
If media
Handle string from the Vertex Suite media wrapper.
Media is handled entirely by the wrapper Media API — no direct Graph calls. Upload a file once; the response gives you a handle (for template creation headers) and a public link (for sending media messages). Accepted types and size caps are below.
# One multipart upload — the wrapper runs Meta's Resumable Upload + hosting for you.
curl -X POST '{{media_api}}/v1/media/upload' \
-H 'Authorization: Bearer {{access_token}}' \
-F 'file=@/path/to/header.jpg'
No Graph API for media. You never call Meta’s Resumable Upload or /media endpoints directly — Atmik Bharat’s wrapper Media API does that for you. Upload once and reuse the response: the handle goes into a template header at creation, and the public link is what you send in media messages. A media id is never used.
Body is the only required component — plain text plus numbered variables ({{1}}, {{2}}…). Footer is an optional single line of plain text with no variables — good for opt-out or brand attribution.
// BODY — numbered variables, one example row per variable
{
"type": "body",
"text": "Hi {{1}}, your order {{2}} ships today.",
"example": { "body_text": [["John", "#A1234"]] }
}
// FOOTER — plain text only, no variables
{
"type": "footer",
"text": "Reply STOP to opt out"
}
Fields
Field
Type
Required?
Notes
body.text
string
Required
Main content; numbered variables only.
example.body_text
array
If variables
Array of example rows; one value per {{n}}.
footer.text
string
Optional
Single line, plain text, no variables.
Limits
Constraint
Value
Body
≤ 1024 chars (the {{n}} tag counts as 1 char)
Body variables
numbered only; can’t be first or last; no two adjacent
Buttons are optional and sit at the bottom. Below is every button type in one array for reference; in practice you mix only what you need (see the combination rules under the table). The table lists each type, its key fields with lengths, and which webhook a tap produces.
How buttons combine. Up to 10 buttons total. You can mix quick reply and call-to-action (url / phone) buttons, but all buttons of the same type must be consecutive — e.g. group your quick replies together and your CTAs together. URL max 2, phone max 1. If a template has more than 3 buttons, WhatsApp shows the first few and tucks the rest under a “See all options” menu. Commerce (catalog / mpm / spm), OTP, and flow buttons are normally the only button on their template — that single button is the action.
Common button limits & rules
Rule
Value
Total buttons
Up to 10 buttons per template
Quick Reply button text
Up to 25 characters
Call-To-Action button text
Up to 25 characters
Phone number CTA buttons
Maximum 1
Website URL CTA buttons
Maximum 2
Quick Reply buttons
Up to 10
Copy Code button
Authentication templates only · up to 15 characters
Auth templates have fixed body/footer — you don’t write copy, you toggle options. The OTP button comes in three flavours: copy-code (user taps to copy), one-tap (autofill, needs your app package + signature hash), and zero-tap (app reads the code automatically). All three full payloads below.
A body message followed by up to 10 swipeable cards. Every card must share the same component structure (same header media type and the same button set).
A marketing template that shows one product (image, title, price from the catalog) with a non-customizable spm View button. Header must be format: "product"; the product itself is chosen at send time. Body uses numbered variables.
# waba_id is required as a query parameter on every call
curl -X POST '{{base_url}}/{{waba_id}}/message_templates?waba_id={{waba_id}}' \
-H 'Authorization: Bearer {{access_token}}' \
-H 'Content-Type: application/json' \
-d '{
"name": "spm_offer",
"language": "en_US",
"category": "marketing",
"components": [
{
"type": "header",
"format": "product"
},
{
"type": "body",
"text": "Use code {{1}} to get {{2}} off our newest succulent!",
"example": { "body_text": [["15OFF", "15%"]] }
},
{
"type": "footer",
"text": "Offer ends soon"
},
{
"type": "buttons",
"buttons": [
{ "type": "spm", "text": "View" }
]
}
]
}'
Body + an order_details checkout button labelled Review and pay. Tapping it opens the native order summary where the customer reviews items and completes payment. The itemized order, shipping and amounts are attached at send time.
Same template for both payment methods. An order_details template only defines the body (+ optional footer) and the Review-and-pay button. Whether the order settles via a payment_gateway or a payment_link, and whether items are catalog (retailer_id) or custom (name), is all decided at send time — you do not need a separate template for each.
# waba_id is required as a query parameter on every call
curl -X POST '{{base_url}}/{{waba_id}}/message_templates?waba_id={{waba_id}}' \
-H 'Authorization: Bearer {{access_token}}' \
-H 'Content-Type: application/json' \
-d '{
"name": "order_checkout",
"language": "en_US",
"category": "utility",
"components": [
{
"type": "body",
"text": "Hi {{1}}, your order {{2}} is ready. Tap below to review the items and pay securely.",
"example": {
"body_text": [
[
"John",
"#A1234"
]
]
}
},
{
"type": "buttons",
"buttons": [
{
"type": "order_details",
"text": "Review and pay"
}
]
}
]
}'
{
"name": "order_checkout",
"language": "en_US",
"category": "utility",
"components": [
{
"type": "body",
"text": "Hi {{1}}, your order {{2}} is ready. Tap below to review the items and pay securely.",
"example": {
"body_text": [
[
"John",
"#A1234"
]
]
}
},
{
"type": "buttons",
"buttons": [
{
"type": "order_details",
"text": "Review and pay"
}
]
}
]
}
Fields
Field
Type
Required?
Notes
components[].buttons[].type
string
Required
"order_details" — the Review and pay checkout button.
button.text
string
Required
Button label, ≤ 25 chars.
body.text
string
Required
Message body; numbered or named variables allowed.
example.body_text
array
Conditional
One example value per body variable.
Limits
Constraint
Value
Category
utility or marketing
Checkout button
one order_details button per template
Order data
items, shipping, totals & currency are supplied at send, not creation
Prerequisite
payments configured & enabled for the WABA
Tap
opens the native Review and pay sheet; result via webhook
A template that pairs with a full itemized order — line items, taxes, shipping, discount and total. It carries a body, an optional footer (e.g. an expiry note) and the order_details button. The order breakdown is sent with the order details send.
Same template for both payment methods. An order_details template only defines the body (+ optional footer) and the Review-and-pay button. Whether the order settles via a payment_gateway or a payment_link, and whether items are catalog (retailer_id) or custom (name), is all decided at send time — you do not need a separate template for each.
# waba_id is required as a query parameter on every call
curl -X POST '{{base_url}}/{{waba_id}}/message_templates?waba_id={{waba_id}}' \
-H 'Authorization: Bearer {{access_token}}' \
-H 'Content-Type: application/json' \
-d '{
"name": "order_details",
"language": "en_US",
"category": "utility",
"components": [
{
"type": "body",
"text": "Hi {{1}}, here is the breakdown for order {{2}}. Review the items, taxes and total, then pay.",
"example": {
"body_text": [
[
"John",
"#A1234"
]
]
}
},
{
"type": "footer",
"text": "Complete payment before the order expires"
},
{
"type": "buttons",
"buttons": [
{
"type": "order_details",
"text": "Review and pay"
}
]
}
]
}'
{
"name": "order_details",
"language": "en_US",
"category": "utility",
"components": [
{
"type": "body",
"text": "Hi {{1}}, here is the breakdown for order {{2}}. Review the items, taxes and total, then pay.",
"example": {
"body_text": [
[
"John",
"#A1234"
]
]
}
},
{
"type": "footer",
"text": "Complete payment before the order expires"
},
{
"type": "buttons",
"buttons": [
{
"type": "order_details",
"text": "Review and pay"
}
]
}
]
}
Fields
Field
Type
Required?
Notes
body.text
string
Required
Intro line above the order card; variables allowed.
footer.text
string
Optional
Single line, ≤ 60 chars, no variables.
components[].buttons[].type
string
Required
"order_details".
button.text
string
Required
Button label, ≤ 25 chars.
Limits
Constraint
Value
Category
utility or marketing
Itemized order
line items, subtotal, tax, shipping, discount supplied at send
Footer
optional; good for an expiry / payment-window note
A transactional template for order status updates — confirmed, processing, shipped, completed or canceled. Body variables carry the order reference, the new status and an optional tracking line. Pairs with the order status send.
# waba_id is required as a query parameter on every call
curl -X POST '{{base_url}}/{{waba_id}}/message_templates?waba_id={{waba_id}}' \
-H 'Authorization: Bearer {{access_token}}' \
-H 'Content-Type: application/json' \
-d '{
"name": "order_status_update",
"language": "en_US",
"category": "utility",
"components": [
{
"type": "body",
"text": "Order {{1}} is now {{2}}. {{3}}",
"example": {
"body_text": [
[
"#A1234",
"shipped",
"Track: https://track.example.com/A1234"
]
]
}
}
]
}'
Status copy with variables (order id, status, tracking).
example.body_text
array
Required
One example value per variable.
footer.text
string
Optional
Optional supporting line, ≤ 60 chars.
Limits
Constraint
Value
Category
utility (transactional)
Status text
driven by body variables
Pairs with
the order_status send, which carries the machine-readable status
Window
sendable anytime as an approved template
04
Template messages — Sending
Send an approved template by filling its header / body / button variables.
Common template envelope — every template send is type: "template" with a template object holding
name, language.code and a components[] array. Components fill the variables of an
already-approved template (header, body, buttons) positionally — they do not define new content. Templates are required for
business-initiated messages outside the 24-hour service window.
Variables — numbered or named
sending
Fill the variables in the order/shape the template was approved with. A template is either numbered or named, never both.
Numbered: placeholders are {{1}}, {{2}} … parameters are positional (order matters, no name).
A button component is type:"button" with a sub_type and a zero-based index — the button’s position in the approved template (first = "0", second = "1"…). The index must match where that button sits; same-type buttons stay consecutive.
// index is the button's ZERO-BASED position in the approved
// template: first button = "0", second = "1", and so on.
// quick_reply
{ "type":"button","sub_type":"quick_reply","index":"0",
"parameters":[ { "type":"payload","payload":"STOP_PROMOS" } ] }
// url (value is appended to the approved URL)
{ "type":"button","sub_type":"url","index":"1",
"parameters":[ { "type":"text","text":"track/123" } ] }
// copy_code (coupon)
{ "type":"button","sub_type":"copy_code","index":"2",
"parameters":[ { "type":"coupon_code","coupon_code":"SAVE20" } ] }
// flow
{ "type":"button","sub_type":"flow","index":"0",
"parameters":[ { "type":"action","action":{ "flow_token":"abc123" } } ] }
// otp (authentication) — sub_type url or copy_code
{ "type":"button","sub_type":"url","index":"0",
"parameters":[ { "type":"text","text":"123456" } ] }
Utility templates are used for transactional and service-related updates such as order confirmations, payment updates, appointment reminders, account alerts, delivery notifications, location-based updates, and call permission requests. Parameters map positionally to the template's {{1}}, {{2}} variables.
Utility, not marketing. Utility templates should be transactional or service-related. Promotional offers, discounts, upsell, cross-sell, or marketing-style content should not be sent as utility templates.
Fields
Field
Type
Required?
Notes
template.name
string
Required
Exact approved template name.
template.language.code
string
Required
e.g. en, en_US, hi.
components[]
array
Conditional
Required only if the template has variables.
components[].type
string
Required
header / body / button.
parameters[].type
string
Required
text / currency / date_time / image …
Limits
Constraint
Value
Category
UTILITY (approved at creation)
Parameters
must match the template's variable count & order
Window
sendable anytime (no 24h limit)
Supported components
1 optional header, 1 required body, 1 optional footer, and up to 10 buttons
Marketing templates are used for promotional or awareness-based messages such as offers, discounts, product launches, announcements, event invites, and re-engagement campaigns.
TTL & pacing. Use a shorter TTL for time-sensitive campaigns such as flash sales, limited-time offers, event reminders, or same-day promotions. Marketing messages should not be repeatedly sent to users who are unlikely to engage.
Fields
Field
Type
Required?
Notes
header parameters[].image.link
string (URL)
If media header
Public URL; matches the header format.
button.sub_type
string
Required
quick_reply / url / phone_number / copy_code …
button.index
string
Required
Zero-based button position.
button parameters[].payload
string
quick_reply
Returned to you on tap.
messages[].button.payload
string
Webhook
The payload you set, echoed on tap.
template.name
string
Required
Exact approved and active marketing template name.
template.language.code
string
Required
Approved template language code, e.g. en, en_US, hi.
components[]
array
Conditional
Required when the template has dynamic variables, media header, or button parameters.
body.parameters[]
array
Conditional
Required when the body contains variables like {{1}}, {{2}}.
body.parameters[].text
string
If text var
Text value used to fill the body variable.
Limits
Constraint
Value
Category
MARKETING
Quick-reply tap
arrives as type:"button" (not interactive)
Per-user pacing
subject to marketing template limits
Supported components
1 optional header, 1 required body, 1 optional footer, and up to 10 buttons
Parameters
Must match the template's variable count and order
Message validity / TTL
Marketing messages may support a configurable validity period; if not delivered within this period it can expire instead of being delivered late
Marketing delivery
Subject to WhatsApp quality, engagement, pacing, and delivery limits. Some messages may be delayed or held for quality checks
Authentication templates are used to send one-time passwords or verification codes for login, account verification, recovery, or security checks. The OTP value is passed in the body and button parameter, and both values must match.
For most basic OTP flows, copy code is the easiest option. One-tap autofill and zero-tap require additional mobile app integration and eligibility checks. If those eligibility checks fail, WhatsApp may display a copy code button instead.
Fields
Field
Type
Required?
Notes
body parameters[0].text
string
Required
The OTP code.
button.sub_type
string
Required
url (one-tap) or copy_code.
button parameters[0].text
string
Required
Must equal the OTP in the body.
template.name
string
Required
Exact approved and active authentication template name.
template.language.code
string
Required
Approved template language code, e.g. en, en_US, hi.
components[]
array
Required
Used to pass the OTP value into the approved authentication template.
button.index
string
Required
Zero-based button position, usually 0.
Limits
Constraint
Value
Category
AUTHENTICATION (fixed body text)
Code
body and button code must match
No webhook
copy/autofill does not send an inbound message
Supported use
OTP, login verification, account verification, account recovery, and security checks
Supported content
Fixed authentication text with optional security disclaimer and optional code expiration warning
Code value
Body OTP and button OTP must match
Buttons
Copy code or one-tap autofill button, based on the approved template
Media / URL / emoji
Not supported in authentication template content
TTL
Configurable validity period from 30 seconds to 15 minutes. Keep the TTL equal to or shorter than the actual OTP expiry time.
Authentication button types
Button type
What it does
Important notes
Copy code
Copies the OTP/code to the user's clipboard. User manually pastes it into the app or website.
Simplest option. Does not require a handshake or app signing hash setup.
One-tap autofill
Opens the app and passes the OTP/code automatically when the user taps the button.
Requires app-side setup. If eligibility checks fail, WhatsApp may fall back to copy code.
Zero-tap
Attempts to deliver the OTP/code automatically without requiring the user to tap a button.
Android only. Requires handshake and app signing key hash setup. If unsupported, can fall back to one-tap autofill or copy code.
Carousel templates display multiple scrollable cards within a single message. Each card can contain its own media header, body content, and buttons, allowing businesses to showcase products, offers, services, promotions or recommendations in a swipeable format.
Limited-time offer templates are marketing templates that display a time-sensitive offer with an expiration countdown. They help create urgency by showing users when an offer, discount, coupon, or promotion will expire.
Catalog templates allow businesses to share an entire WhatsApp catalog within a template message. Users can browse available products directly inside WhatsApp and continue shopping without leaving the conversation.
Catalog templates do not send product information directly in the message payload. Products are retrieved dynamically from the connected WhatsApp catalog when the user opens the catalog.
Single Product Message (SPM) templates promote a specific catalog product within a template message. Product information such as image, title, price, and availability is automatically loaded from the connected catalog using the product parameter, while body variables are populated dynamically at send time.
Multi-Product Message (MPM) templates allow businesses to showcase multiple catalog products grouped into one or more sections. Users can browse products, add items to their cart, and submit an order directly within WhatsApp.
An MPM displays a selected set of catalog products organized into sections — up to 10 sections and up to 30 products in total. In contrast, a Catalog Template opens the fully connected WhatsApp catalog through a catalog button, letting users browse all available products rather than a predefined selection.
Fields
Field
Type
Required?
Notes
button.sub_type
string
Required
mpm.
action.sections[]
array
Required
title + product_items[].
product_items[].product_retailer_id
string
Required
SKU from your catalog.
thumbnail_product_retailer_id
string
Required
Product used as the message thumbnail.
sections[].title
string
Required
Section title displayed to the user.
Limits
Constraint
Value
Sections
up to 10, up to 30 products total
Order
arrives as type:"order"
Category
MARKETING
Products
Up to 30 products across all sections
Thumbnail product
Must exist in the connected catalog
Catalog ownership
Catalog must belong to or be shared with the sending WhatsApp Business Account
Flow templates launch a WhatsApp Flow from a template button. Flows are used for multi-step forms and structured journeys such as appointment booking, lead capture, registration, surveys, feedback, and service requests. When the user completes the Flow, the submitted answers are received through an nfm_reply webhook in response_json.
Sends the approved checkout template with the full order_details action — header asset, body variables, shipping info and the itemized order. The customer taps Review and pay; the result returns on the payment webhook. (shipping_info is required for physical-goods; omit it for digital-goods.)
Two payment methods — set payment_settings[].type to payment_gateway (native in-WhatsApp checkout via a gateway configured in WhatsApp Manager) or payment_link (the customer taps through to a hosted payment URL you supply). The rest of the payload is identical — same cURL, just swap the payment_settings block (see the gateway vs payment-link tabs). Exact provider fields follow Meta’s payments spec for your region.
Catalog or custom products — each order.items[] entry can reference a product from your connected catalog via retailer_id (name & image come from the catalog) or be a custom, ad-hoc product defined inline with name + amount. You can mix both in one order; every item still needs amount and quantity.
Sends the itemized order with the Review and pay button. The two request tabs below are identical except for order.items[] — one uses catalog products (retailer_id), the other custom products (name). The Payment link tab shows the same call with payment_settings swapped.
Two payment methods — set payment_settings[].type to payment_gateway (native in-WhatsApp checkout via a gateway configured in WhatsApp Manager) or payment_link (the customer taps through to a hosted payment URL you supply). Only the payment_settings block changes — see the Payment link tab.
Catalog vs custom products — the two examples below are identical except for order.items[]. A catalog item uses retailer_id (name & image come from your connected catalog); a custom item is defined inline with name. Both still send amount + quantity, and you may mix the two in one order.
Pushes a status update for a previously sent order, referenced by reference_id. The body shows the human-readable message while the order_status action updates the machine-readable state on the existing order card.
updates the status shown on the existing order card
Window
sendable anytime as an approved template
05
Service Messages
Free-form replies inside the 24-hour window: standard, interactive & commerce.
Service messages are free-form messages sent within an active customer service conversation. Outside the customer service window, businesses must use approved template messages.
Common envelope — present in every service-message payload below; the per-type tables only list the type-specific object.
Field
Type
Required?
Notes
messaging_product
string
Required
Always "whatsapp".
recipient_type
string
Optional
"individual" (default).
to
string
Required
Recipient phone (E.164, with country code).
type
string
Required
Message type, e.g. text / image / interactive …
RequiredOptionalConditionalWebhook— Conditional = required only in certain cases (noted).
Text Messages are free-form service messages used to send plain text content within an active customer service conversation. Link previews can be enabled for supported URLs using the preview_url parameter.
# waba_id is required as a query parameter on every call
curl -X POST '{{base_url}}/{{phone_number_id}}/messages?waba_id={{waba_id}}' \
-H 'Authorization: Bearer {{access_token}}' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "{{recipient_phone}}",
"type": "text",
"text": {
"preview_url": true,
"body": "As requested, here is the link to our latest product: https://www.example.com/product"
}
}'
{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "{{recipient_phone}}",
"type": "text",
"text": {
"preview_url": true,
"body": "As requested, here is the link to our latest product: https://www.example.com/product"
}
}
Audio messages send an audio file within an active customer service conversation. Audio can be sent using a public media URL. Captions are not supported for audio messages.
# waba_id is required as a query parameter on every call
curl -X POST '{{base_url}}/{{phone_number_id}}/messages?waba_id={{waba_id}}' \
-H 'Authorization: Bearer {{access_token}}' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "{{recipient_phone}}",
"type": "audio",
"audio": {
"link": "{{media_audio_url}}"
}
}'
Image messages send images within an active customer service conversation. Images can be sent using a public media URL and may include an optional caption.
# waba_id is required as a query parameter on every call
curl -X POST '{{base_url}}/{{phone_number_id}}/messages?waba_id={{waba_id}}' \
-H 'Authorization: Bearer {{access_token}}' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "{{recipient_phone}}",
"type": "image",
"image": {
"link": "{{media_image_url}}",
"caption": "Optional caption text"
}
}'
Video messages send a video file within an active customer service conversation. Videos are delivered from a public URL and may include an optional caption.
# waba_id is required as a query parameter on every call
curl -X POST '{{base_url}}/{{phone_number_id}}/messages?waba_id={{waba_id}}' \
-H 'Authorization: Bearer {{access_token}}' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "{{recipient_phone}}",
"type": "video",
"video": {
"link": "{{media_video_url}}",
"caption": "Optional caption text"
}
}'
Document messages send files such as PDFs, Office documents, text files, or other supported documents within an active customer service conversation. The message displays a document icon and filename that the WhatsApp user can tap to download. Documents are delivered from a public URL and can include an optional caption.
# waba_id is required as a query parameter on every call
curl -X POST '{{base_url}}/{{phone_number_id}}/messages?waba_id={{waba_id}}' \
-H 'Authorization: Bearer {{access_token}}' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "{{recipient_phone}}",
"type": "document",
"document": {
"link": "{{media_document_url}}",
"caption": "Optional caption text",
"filename": "document.pdf"
}
}'
Sticker messages send static or animated WebP stickers within an active customer service conversation. Stickers are delivered from a public URL and do not support captions.
# waba_id is required as a query parameter on every call
curl -X POST '{{base_url}}/{{phone_number_id}}/messages?waba_id={{waba_id}}' \
-H 'Authorization: Bearer {{access_token}}' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "{{recipient_phone}}",
"type": "sticker",
"sticker": {
"link": "{{media_sticker_url}}"
}
}'
Contact messages share one or more contact cards within a WhatsApp conversation. Each contact can include names, phone numbers, email addresses, company information, websites, addresses, and other supported contact details.
Location messages send a map pin to the user using latitude and longitude. An optional name and address can be included to label the pin and make the location easier to understand.
# waba_id is required as a query parameter on every call
curl -X POST '{{base_url}}/{{phone_number_id}}/messages?waba_id={{waba_id}}' \
-H 'Authorization: Bearer {{access_token}}' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "{{recipient_phone}}",
"type": "location",
"location": {
"latitude": "21.1938",
"longitude": "81.3509",
"name": "Atmik Bharat Industries",
"address": "Bhilai, Chhattisgarh, India"
}
}'
Reply Button Messages are an interactive WhatsApp message type, not a template message. They show up to 3 quick-reply buttons below a message body, with optional header and footer. When the user taps a button, a button_reply webhook is returned.
List Messages are an interactive WhatsApp message type that displays a button, not a template message. They present multiple options in a structured menu where the user taps a button to open the list and selects one option. Useful for service selection, support menus, appointment types, locations, product categories, and guided workflows. Selecting a row returns a list_reply webhook.
CTA URL Messages are an interactive WhatsApp message type that display a single tappable button. When tapped, the button opens the configured URL in the user's browser. Opening the URL does not generate an inbound webhook.
# waba_id is required as a query parameter on every call
curl -X POST '{{base_url}}/{{phone_number_id}}/messages?waba_id={{waba_id}}' \
-H 'Authorization: Bearer {{access_token}}' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "{{recipient_phone}}",
"type": "interactive",
"interactive": {
"type": "cta_url",
"header": { "type": "text", "text": "Special offer" },
"body": { "text": "Tap below to view the offer on our website." },
"footer": { "text": "Powered by Atmik Bharat" },
"action": {
"name": "cta_url",
"parameters": {
"display_text": "Visit website",
"url": "https://www.example.com/offer"
}
}
}
}'
Media Carousel Messages are interactive WhatsApp messages that show 2 to 10 horizontally scrollable media cards. Each card contains an image or video header, optional body text, and either a CTA URL action or quick-reply button action.
The main interactive message supports body text only. Header, footer, and other interactive components are not supported at the main message level for media carousel messages.
Fields
Field
Type
Required?
Notes
interactive.type
string
Required
Must be "carousel".
interactive.body.text
string
Required
Main body. Max 1024.
action.cards
array
Required
2–10 cards.
cards[].header
object
Required
image/video via link.
cards[].action
object
Required
cta_url params OR quick_reply buttons[] — must match across cards.
Launches a multi-screen Flow form via a CTA button inside the 24-hour window. On completion you receive an nfm_reply webhook with answers in response_json.
Native address form. Availability: India (IN) and Singapore (SG) only. The submitted form arrives as an nfm_reply webhook (name "address_message"), with the fields inside response_json.
# waba_id is required as a query parameter on every call
curl -X POST '{{base_url}}/{{phone_number_id}}/messages?waba_id={{waba_id}}' \
-H 'Authorization: Bearer {{access_token}}' \
-H 'Content-Type: application/json' \
-d '{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": "{{recipient_phone}}",
"type": "interactive",
"interactive": {
"type": "address_message",
"body": { "text": "Please provide your delivery address." },
"action": {
"name": "address_message",
"parameters": {
"country": "IN",
"values": {
"name": "John Doe",
"phone_number": "+919999999999",
"in_pin_code": "490001",
"house_number": "12",
"floor_number": "2",
"tower_number": "A",
"building_name": "Sunrise Apartments",
"address": "Civic Centre Road",
"landmark_area": "Near City Mall",
"city": "Bhilai",
"state": "Chhattisgarh"
}
}
}
}
}'
in_pin_code, house_number, floor_number, building_name, landmark_area, city, state…
SG values
sg_post_code, unit_number, floor_number…
Webhook
nfm_reply.name = "address_message"
Commerce
Commerce messages require a connected Commerce Manager catalog. When a customer builds a cart from any of them, the
submission arrives as a single type: "order" message in the messages[] webhook — shown on the
Webhook response tab.
On preview images & media: all media is sent by public link (never an uploaded media id) — the URL must be reachable by Meta's servers at send time. For webhooks, always read messages[].type first and branch: interactive replies live under interactive (button_reply / list_reply / nfm_reply), carts under order, shared pins under location. response_json is a JSON string — JSON.parse() it inside a try/catch.