Send a Template Message
Template messages are the only way to initiate a WhatsApp conversation or send messages outside the 24-hour customer service window. This guide shows you how to send templates with variable substitution.
No approved template yet? Adopt one from Meta's Template Library — unchanged library templates are created already approved, and the id you get back is sent exactly like any other template below.
Required Scopes
| Scope | Purpose |
|---|---|
messages:create | Send template messages |
Send Template Message
POST /api/v1/whatsapp-templates/{template_id_or_name}/send
The path segment accepts either the template's DB ID (a canonical UUID, e.g. f47ac10b-58cc-4372-a567-0e02b2c3d479) or the template's WhatsApp name (e.g. order_confirmation). A 36-character canonical UUID is treated as the ID; anything else is treated as a name.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
contact_id | string | One recipient field required¹ | The contact to send to. Uses the contact's first WhatsApp contact method, falling back to its phone number. |
contact_method_id | string | One recipient field required¹ | A contact_methods.id of type whatsapp_id or whatsapp_bsuid. Pins both the recipient and the owning contact. If contact_id is also sent it must name that same contact, otherwise the request returns 400. |
whatsapp_id | string | One recipient field required¹ | Send straight to a phone number (no +), an alphanumeric WhatsApp id, or a Business-scoped user ID such as US.13491208655302741918, without looking up a contact first. A contact is matched or created automatically. |
conversation_id | string | One recipient field required¹ | Send within an existing conversation (must be a WhatsApp conversation). Also disambiguates a template name shared across channels/languages. |
channel_id | string | No | The WhatsApp channel to send from. Optional — see Choosing the channel. |
language | string | No | The template language to send when the name exists in more than one language. A Meta language/locale code (e.g. en, de, en_US, pt_BR). Optional — see Choosing the language. |
variable_values | string[] | object | No | Values for the body placeholders. Shape depends on the template's parameter_format (see below). An empty value counts as not set. |
variable_mapping | object | No | Contact fields to fill variables from, for this send only. Keyed like variable_values ("1" or "first_name"). See Mapping variables to contact fields. |
variable_fallbacks | object | No | Fixed text per variable, used when the variable would otherwise be empty. Keyed like variable_values. |
header_media_url | string | No | Header media for IMAGE/VIDEO/DOCUMENT templates. Accepts a bare attachment ID (UUID), a SendSeven proxy URL, a permanent public URL, a legacy GCS object ID, or an external https URL. |
header_document_filename | string | No | DOCUMENT headers only. Overrides the filename WhatsApp shows for the document. Must include the correct extension matching the file (e.g. invoice.pdf for a PDF, report.docx for a Word file). Ignored for IMAGE/VIDEO/TEXT headers. See Document headers and filenames. |
cards | object[] | No | CAROUSEL templates only. Per-card parameters, supplied positionally — entry 0 fills the first card, entry 1 the second, and so on. Each entry has its own variable_values (same shape rules as the top-level field) and header_media_url. Omit for non-carousel templates. See Carousel templates. |
¹ At least one of contact_id, contact_method_id, whatsapp_id, or conversation_id must be present. channel_id and language are not recipients — they only select the sending channel and language variant — so they cannot satisfy this requirement on their own. Omitting all four recipient fields returns 422.
Identifying the recipient
You can address the recipient four different ways. Send any one of these fields (you may combine some — see the priority rules). This makes the endpoint flexible for both contact-centric apps and stateless integrations that just have a phone number.
| Field | Use when… | Contact handling |
|---|---|---|
contact_id | You already manage contacts in SendSeven. | Uses the contact's first whatsapp_id method, then its first whatsapp_bsuid method, falling back to its phone number. |
contact_method_id | A contact has multiple WhatsApp identifiers and you must target a specific one. | Pins the exact method and its owning contact. |
whatsapp_id | You just have a phone number / WhatsApp id / Business-scoped user ID (typical for integrations). | An existing contact with that identifier is reused; otherwise a new contact and method are created. |
conversation_id | You're continuing a specific existing thread. | Derived from the conversation. |
Resolution priority (first match wins)
When more than one recipient field is supplied, they are resolved in this fixed order:
contact_method_id— the method's value is the recipient and its owning contact is used. The method must be of typewhatsapp_idorwhatsapp_bsuid(any other type →400). Ifcontact_idis also sent it must name the method's owning contact — a mismatch returns400(it is no longer silently ignored).conversation_id— the conversation must belong to a WhatsApp channel (400otherwise). The recipient and contact are taken from the conversation.whatsapp_id— the value is normalized (a leading+is stripped; spaces, dashes and parentheses are tolerated on phone-shaped values; alphanumeric ids are kept as-is; a Business-scoped user ID is kept verbatim, periods included):- If
contact_idis also sent, that contact is used and gains a matching WhatsApp contact method with this value (created if it doesn't already exist). - Otherwise, an existing contact is matched by this identifier; if none exists, a new contact + matching WhatsApp method are created.
- A Business-scoped user ID becomes a
whatsapp_bsuidmethod scoped to the sending channel; a phone-shaped or alphanumeric id becomes a workspace-widewhatsapp_idmethod.
- If
contact_id— uses the contact's firstwhatsapp_idmethod, then its firstwhatsapp_bsuidmethod, falling back to its phone number.
Business-scoped user IDs (WhatsApp usernames)
A contact who adopted a WhatsApp username has no phone number and no whatsapp_id — only a whatsapp_bsuid method. Templates work for them with no special handling: pass the contact_id (or the contact_method_id of the whatsapp_bsuid method), and SendSeven addresses the message with the Business-scoped ID.
curl -X POST "https://api.sendseven.com/api/v1/whatsapp-templates/order_shipped/send" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"whatsapp_id": "US.13491208655302741918",
"channel_id": "ch_123",
"variable_values": ["Alex", "TRK-99812"]
}'
Two constraints apply:
- A Business-scoped ID only works from the WhatsApp Business portfolio that issued it, so pass the
channel_idof the channel it belongs to when sending by a barewhatsapp_id. - Authentication templates cannot be sent to a Business-scoped user ID. WhatsApp rejects them because one-tap, zero-tap and copy-code buttons require a phone number, so the send fails with an explanatory error. Deliver the code with a non-authentication template, or ask the contact for their phone number first — see Migrating to BSUIDs.
Choosing the channel
Templates belong to the WhatsApp Business Account, not to one number, so a template can be sent from any active WhatsApp channel of the same account in your workspace. The sending channel is chosen like this:
channel_id— sent from that channel. It must be an active, connected WhatsApp channel of your workspace in the same WhatsApp Business Account as the template; otherwise the request returns400witherror_codetemplate_channel_mismatchand nothing is sent.- else
conversation_id— sent from the conversation's channel. That channel must be in the template's account. - else the template's own channel (the channel of the template ID you referenced, or of the copy picked by name).
A message is never stored in a conversation on a different number than the one that sent it. If you pass both conversation_id and a channel_id that is not the conversation's channel, or a conversation_id whose channel is in a different WhatsApp Business Account than the template, the request returns 400 with error_code conversation_channel_mismatch and nothing is sent. To reply in a conversation, omit channel_id.
If your workspace uses Custom Inboxes, a member whose inboxes don't grant a WhatsApp number can't send a template from it without a conversation: the request returns 403 with error_code channel_access_denied. Owners, admins and API tokens are not restricted.
The message, the conversation, billing and delivery statuses all belong to the channel that actually sent it. When that channel has its own copy of the template, its saved settings (variable_mapping, default header media) are used.
When you reference a template by name and it exists on several channels, the copy is picked the same way: the channel_id's account, else the conversation_id's channel or account, else the first matching channel (channel ambiguity never raises an error). A name that doesn't exist in the channel_id's account returns 400. (Language ambiguity is handled separately — see below.)
channel_id also selects the sending channel when you send by whatsapp_id with no contact or conversation.
Choosing the language
The same template name can exist as several rows — one per language. After the channel is settled, the language variant is chosen as follows:
- By DB ID — the language is fixed by that row. If you also pass a
languagethat differs from the row's language, the request returns400. (Reference the template by name to choose a language.) - By name, only one language exists — that language is used;
languageis optional. - By name, multiple languages exist:
- if you pass
language, that exact variant is used —400if the name has no variant in that language (the error lists the available languages); - otherwise SendSeven auto-resolves from the recipient contact's preferred language (
Contact.language), matching the code exactly first, then by base-language family (sodematchesde_DE, andpt_BRmatchespt); - if there is no
languageand the contact has no language (or none that matches an available variant), the request returns400asking you to specifylanguageor set the contact's language.
- if you pass
Because language auto-resolution reads the recipient contact, sending by a bare whatsapp_id that creates a brand-new contact (which has no language yet) against a multi-language template will return the 400 in step 3 unless you pass language explicitly.
language must be the exact Meta language/locale code stored on the template (e.g. en, de, en_US, pt_BR). All language variants of a template share the same parameter shape, so variable_values is identical regardless of which language is selected.
Document headers and filenames
For templates with a DOCUMENT header, WhatsApp displays a filename on the document bubble. You can control it with header_document_filename. When you do not pass one, SendSeven resolves the filename using this precedence:
header_document_filenamein the request (per-send override), else- the template's
default_header_document_filename(the default you can set per template under Settings → WhatsApp Templates, or via the template create/update API), else - the original filename of the uploaded attachment, when
header_media_urlresolves to a file uploaded through SendSeven (attachment ID, proxy/public URL, or GCS ID), else - nothing — the field is omitted and WhatsApp decides what to display. This is the case for external
httpsURLs with no stored filename.
The filename must include the correct file extension for the document's MIME type (e.g. .pdf, .docx, .xlsx). A mismatched or missing extension can cause WhatsApp to display the document incorrectly or to reject it.
header_document_filename never applies to a carousel. Carousel card headers are IMAGE or VIDEO only — there is no DOCUMENT (or TEXT) header on a card. Supply each card's image/video via that card's header_media_url in the cards array (see Carousel templates).
curl (document template with filename override)
curl -X POST "https://api.sendseven.com/api/v1/whatsapp-templates/tpl_invoice_789/send" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "contact_xyz789",
"variable_values": ["John", "ORD-5678"],
"header_media_url": "9f3c8b1a-2d44-4e6f-8a91-0b7c6d5e4f3a",
"header_document_filename": "invoice-ORD-5678.pdf"
}'
Recipient examples
These examples all use the same positional template tpl_abc123; swap in your own template ID/name and recipient field. See Variable Structure below for variable_values shapes.
Send to a contact (contact_id)
curl -X POST "https://api.sendseven.com/api/v1/whatsapp-templates/tpl_abc123/send" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "contact_xyz789",
"variable_values": ["John", "ORD-5678"]
}'
Send to a specific WhatsApp number on a contact (contact_method_id)
Use this when a contact has more than one WhatsApp identifier and you must target one exactly. The method must be of type whatsapp_id or whatsapp_bsuid.
curl -X POST "https://api.sendseven.com/api/v1/whatsapp-templates/tpl_abc123/send" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"contact_method_id": "cm_9f3c8b1a2d44",
"variable_values": ["John", "ORD-5678"]
}'
Send straight to a phone number or WhatsApp id (whatsapp_id)
No contact lookup required — ideal for integrations. The number is given without a + (a leading + is stripped automatically). A contact is matched by this identifier, or created if none exists.
curl -X POST "https://api.sendseven.com/api/v1/whatsapp-templates/tpl_abc123/send" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"whatsapp_id": "447700900123",
"variable_values": ["John", "ORD-5678"]
}'
To attach that number to an existing contact (creating the whatsapp_id method if missing), send both contact_id and whatsapp_id:
curl -X POST "https://api.sendseven.com/api/v1/whatsapp-templates/tpl_abc123/send" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "contact_xyz789",
"whatsapp_id": "447700900123",
"variable_values": ["John", "ORD-5678"]
}'
Send within an existing conversation (conversation_id)
The conversation must belong to a WhatsApp channel.
curl -X POST "https://api.sendseven.com/api/v1/whatsapp-templates/tpl_abc123/send" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": "b5d8e2a1-4c3f-4a8b-9d2e-1f6a7b8c9d0e",
"variable_values": ["Sarah", "INV-2026-001"]
}'
Pin the sending channel (channel_id)
Add channel_id to choose which number sends the template. It can be any active WhatsApp channel of the template's WhatsApp Business Account (see Choosing the channel). channel_id is always optional and may be combined with any recipient field.
curl -X POST "https://api.sendseven.com/api/v1/whatsapp-templates/order_confirmation/send" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"whatsapp_id": "447700900123",
"channel_id": "channel_wa_001",
"variable_values": ["John", "ORD-5678"]
}'
Pick the template language (language)
When a template name exists in several languages, pass language to choose the variant. You can combine it with any recipient field and with channel_id.
curl -X POST "https://api.sendseven.com/api/v1/whatsapp-templates/order_confirmation/send" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "contact_xyz789",
"language": "de",
"variable_values": ["John", "ORD-5678"]
}'
If you omit language, the contact's preferred language (Contact.language) is used when it matches an available variant; otherwise the request returns 400 listing the available languages.
Variable Structure
Every WhatsApp template has a parameter_format of either positional ({{1}}, {{2}}, ...) or named ({{name}}, {{order_number}}, ...). The shape you pass for variable_values must match.
Positional templates
For templates with parameter_format: "positional", pass variable_values as either a list (recommended) or a dict keyed by the placeholder number.
List form (recommended):
{
"variable_values": ["John", "ORD-5678", "Feb 15, 2026"]
}
The first item fills {{1}}, the second fills {{2}}, and so on.
Equivalent dict form:
{
"variable_values": {"1": "John", "2": "ORD-5678", "3": "Feb 15, 2026"}
}
Numeric string keys are sorted by their integer value before substitution, so the order of keys in the JSON does not matter.
Named templates
For templates with parameter_format: "named", pass variable_values as an object whose keys match the template's named placeholders.
{
"variable_values": {
"name": "John",
"quote": "ORD-5678",
"due_date": "Feb 15, 2026"
}
}
The order of keys does not matter. Meta resolves named parameters by parameter_name, so you can list keys in any order as long as each key matches a placeholder in the template body.
Carousel templates
A carousel template has a top-level BODY and up to 10 cards. Fill them with two things:
- the top-level
variable_valuesfor the carousel's own body text, exactly as above; - a
cardsarray for the per-card body text, supplied positionally — entry 0 is the first card, entry 1 the second, and so on. Provide one entry per card you want to personalize.
Each entry in cards is an object with:
| Field | Type | Description |
|---|---|---|
variable_values | string[] | object | Values for this card's body placeholders. Same positional/named rules (and same reserved keys — url_suffix, otp_code, and their indexed variants) as the top-level field. Must match the template's parameter_format. |
header_media_url | string | Media for this card's IMAGE/VIDEO header. Accepts the same forms as the top-level header_media_url (attachment ID, proxy/public URL, GCS ID, or external https URL). |
Every card follows the template's single parameter_format — you cannot mix positional and named across cards.
curl -X POST "https://api.sendseven.com/api/v1/whatsapp-templates/weekly_deals/send" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "contact_xyz789",
"variable_values": ["Alex"],
"cards": [
{
"variable_values": ["-30%"],
"header_media_url": "9f3c8b1a-2d44-4e6f-8a91-0b7c6d5e4f3a"
},
{
"variable_values": ["-20%"],
"header_media_url": "https://cdn.example.com/shoes.jpg"
}
]
}'
Here the top-level variable_values fills the carousel body (Check out this week's featured deals, {{1}}!), while cards[0] and cards[1] fill each card's own {{1}} and supply its header image. A card whose variable_values shape does not match the template's parameter_format returns 400 naming the offending card (e.g. "Card 1 uses positional parameters; …").
Mapping variables to contact fields
Instead of sending literal values, you can tell SendSeven which contact field fills each variable with variable_mapping. Each value is one of:
| Mapping | Fills the variable with |
|---|---|
contact.first_name, contact.last_name, contact.name | The contact's name (or part of it) |
contact.email, contact.phone, contact.birthday | The contact's primary email, phone number, or birthday (YYYY-MM-DD) |
custom_field.<key> | A custom field of the contact (contact.custom.<key> also works) |
tenant.name | Your workspace name |
agent.name, agent.nickname | The name or chat nickname of the team member sending. Empty when you send with an API key. |
custom:<text> | The fixed text after custom: |
GET /api/v1/whatsapp-templates/fields lists these mapping paths. Its custom_field.<key> entries appear only when your API key also has custom_fields:read.
variable_fallbacks adds a fixed text per variable for contacts where the field is empty.
For every variable, SendSeven uses the first of these that gives a value:
- a non-empty
variable_valuesentry variable_mappingfrom the request- the mapping saved on the template (Settings > WhatsApp templates)
variable_fallbacks, if the value above is empty
If a variable is still empty after these steps, nothing is sent and the request returns 400. WhatsApp rejects empty template parameters, so the request fails before it reaches WhatsApp:
{
"detail": {
"message": "Missing value for template parameter(s): 1, 3",
"error_code": "missing_template_parameters"
}
}
curl -X POST "https://api.sendseven.com/api/v1/whatsapp-templates/tpl_abc123/send" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "c1d2e3f4-5678-9abc-def0-1234567890ab",
"variable_mapping": {
"1": "contact.first_name",
"2": "custom_field.customer_number"
},
"variable_fallbacks": {"1": "there"}
}'
POST /api/v1/whatsapp-templates/{template_id}/preview accepts the same variable_values, variable_mapping and variable_fallbacks together with a contact_id. It shows the text this contact would receive. The response lists each variable under variables (with the mapping used, the value, and where it came from: value, mapping, template_default, fallback or empty). Variables that would block the send are listed under missing and stay as {{1}} in the preview text.
Knowing which format to use
Fetch the template via GET /api/v1/whatsapp-templates/{template_id} and check the parameter_format field on the response. Branch your client code on that value:
{
"id": "tpl_abc123",
"name": "order_confirmation",
"parameter_format": "named",
"...": "..."
}
A template's parameter_format only changes when the template is edited, so you can cache it alongside the template ID and refresh the cache after an edit.
Error cases
If you send the wrong shape for a template's parameter_format, the API returns 400 with one of these messages in detail:
| Trigger | Response message |
|---|---|
Sent a list against a template with parameter_format: "named" | Template uses named parameters; pass values as an object keyed by parameter name |
Sent a name-keyed dict against a template with parameter_format: "positional" | Template uses positional parameters; pass values as a list or as a dict keyed by '1', '2', ... |
Sent the wrong shape for a carousel card's variable_values (index N) | Same messages as above but prefixed with the card, e.g. Card N uses positional parameters; pass values as a list or as a dict keyed by '1', '2', ... |
A variable is still empty after variable_values, variable_mapping, the template's saved mapping and variable_fallbacks | Missing value for template parameter(s): 1, 3 (in detail.message, with detail.error_code: "missing_template_parameters") |
curl (positional template, list form)
curl -X POST "https://api.sendseven.com/api/v1/whatsapp-templates/tpl_abc123/send" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "contact_xyz789",
"variable_values": ["John", "ORD-5678", "Feb 15, 2026"]
}'
curl (named template)
curl -X POST "https://api.sendseven.com/api/v1/whatsapp-templates/tpl_named_456/send" \
-H "Authorization: Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "contact_xyz789",
"variable_values": {
"name": "John",
"quote": "ORD-5678",
"due_date": "Feb 15, 2026"
}
}'
Python
import requests
BASE_URL = "https://api.sendseven.com/api/v1"
HEADERS = {
"Authorization": "Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00",
"Content-Type": "application/json",
}
# Positional template -- pass a list
positional_response = requests.post(
f"{BASE_URL}/whatsapp-templates/tpl_abc123/send",
headers=HEADERS,
json={
"contact_id": "contact_xyz789",
"variable_values": ["John", "ORD-5678", "Feb 15, 2026"],
},
)
# Named template -- pass a dict keyed by parameter name (order is irrelevant)
named_response = requests.post(
f"{BASE_URL}/whatsapp-templates/tpl_named_456/send",
headers=HEADERS,
json={
"contact_id": "contact_xyz789",
"variable_values": {
"name": "John",
"quote": "ORD-5678",
"due_date": "Feb 15, 2026",
},
},
)
print(named_response.json()["message_id"])
JavaScript
const BASE_URL = "https://api.sendseven.com/api/v1";
const HEADERS = {
"Authorization": "Bearer s7_api_a1b2c3d4e5f6789012345678abcdef00",
"Content-Type": "application/json",
};
// Positional template -- pass an array
await fetch(`${BASE_URL}/whatsapp-templates/tpl_abc123/send`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
contact_id: "contact_xyz789",
variable_values: ["John", "ORD-5678", "Feb 15, 2026"],
}),
});
// Named template -- pass an object keyed by parameter name
const response = await fetch(`${BASE_URL}/whatsapp-templates/tpl_named_456/send`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
contact_id: "contact_xyz789",
variable_values: {
name: "John",
quote: "ORD-5678",
due_date: "Feb 15, 2026",
},
}),
});
const result = await response.json();
console.log(`Template sent: ${result.message_id}`);
Response
A successful send returns the SendTemplateResponse shape:
{
"success": true,
"message_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"external_id": "wamid.HBgNNDkxNTIxNTU4MDg2NRUCABIYFjNFQjBCNUYxMjdFRkI5",
"conversation_id": "b5d8e2a1-4c3f-4a8b-9d2e-1f6a7b8c9d0e",
"error": null,
"error_code": null,
"error_details": null
}
| Field | Type | Description |
|---|---|---|
success | boolean | true when the template was accepted by WhatsApp |
message_id | string | null | Internal SendSeven message ID |
external_id | string | null | WhatsApp message ID (wamid…) |
conversation_id | string | Conversation the message was sent in (new or existing) |
error | string | null | Human-readable error when success is false |
error_code | integer | null | WhatsApp API error code (e.g. 131008 missing parameters, 131042 payment issue) |
error_details | string | null | Additional detail from Meta (e.g. which parameter is missing) |
On a successful call the response success field is always true. If WhatsApp rejects the send, the API returns HTTP 400 with a detail object carrying the failure, e.g. {"detail": {"message": "…", "error_code": 131008, "error_details": "…"}} — the error* fields on the success schema above are only populated in edge cases, so treat a non-2xx status as the failure signal.
Values are sent as written
Everything in variable_values (including each card's variable_values) is sent exactly as you write it. A value such as "contact.name" is not replaced: the customer would receive the text "contact.name". To fill a variable from the contact's data, use variable_mapping for this send, or save a mapping on the template (PUT /api/v1/whatsapp-templates/{template_id}/variable-mapping). A placeholder you leave out of variable_values is then filled from the mapping:
{
"contact_id": "contact_xyz789",
"variable_values": { "2": "ORD-5678", "3": "Feb 15, 2026" },
"variable_mapping": { "1": "contact.name" }
}
agent.name and agent.nickname are the team member who sends the message. When you send with an API key they are empty, so add a variable_fallbacks value for them or the send fails with missing_template_parameters.
Carousel cards
Each card is resolved against its own body placeholders: cards[0].variable_values fills card 0's {{1}} (or its named placeholders), independently of the top-level body and of the other cards. Card values are always sent as written; variable_mapping and the template's saved mapping apply to the top-level header and body only.
{
"contact_id": "contact_xyz789",
"variable_mapping": { "1": "contact.first_name" },
"cards": [
{ "variable_values": ["-30%"], "header_media_url": "9f3c8b1a-2d44-4e6f-8a91-0b7c6d5e4f3a" },
{ "variable_values": ["-20%"], "header_media_url": "https://cdn.example.com/shoes.jpg" }
]
}
Error Responses
Errors from this endpoint come in three shapes. See Error Handling for the general rules.
- Request checks return
detailas a message string (for example a wrongvariable_valuesshape). - Send failures return
400with an object:{"detail": {"message": "...", "error_code": ..., "error_details": "..."}}.error_codeis present only when there is a code: the stringmissing_template_parameters, or WhatsApp's numeric error code (for example131008missing parameters,132001template does not exist). When WhatsApp's code is138000,detail.is_coexistencetells you whether the number is connected through the WhatsApp Business app. - Schema errors return
422withdetailas a list of field errors.
| Status | detail | Description |
|---|---|---|
| 400 | "Template uses named parameters; ...", "Template uses positional parameters; ...", "Missing values for named parameters: [...]" | variable_values doesn't match the template's parameter_format (see Error cases) |
| 400 | {"message": "Missing value for template parameter(s): 1, 3", "error_code": "missing_template_parameters"} | A variable is still empty after mapping and fallbacks |
| 400 | {"message": "Template not approved. Current status: ..."} | The template is not approved |
| 400 | {"message": "contact_method_id belongs to a different contact than the provided contact_id. ..."} | contact_method_id and contact_id name different contacts |
| 400 | {"message": "conversation_id must reference a WhatsApp conversation"} | conversation_id is not a WhatsApp conversation |
| 400 | {"message": "Invalid whatsapp_id: ..."} | whatsapp_id is not a valid WhatsApp ID or Business-scoped user ID |
| 400 | {"message": "Contact has no WhatsApp ID or phone number"} | The resolved recipient can't be reached on WhatsApp |
| 400 | {"message": "Contact not found"}, {"message": "Contact method not found"}, {"message": "Conversation not found"} | A recipient ID is unknown or belongs to another workspace |
| 400 | {"message": "...", ...} | Other recipient, channel and language conflicts: a contact_method_id that isn't whatsapp_id or whatsapp_bsuid, an authentication template addressed to a Business-scoped user ID, a language that doesn't match the template, a channel_id outside the template's WhatsApp Business Account (error_code: template_channel_mismatch), a conversation_id on a different number than the sending one (error_code: conversation_channel_mismatch), or a name that exists in several languages without language |
| 400 | {"message": "...", "error_code": 131008, "error_details": "..."} | WhatsApp rejected the send |
| 401 | "Invalid or expired API token" | Token is invalid or expired |
| 403 | "Missing required permission(s): ..." | Token lacks the scope |
| 403 | "You don't have access to this WhatsApp number." | Member restricted by Custom Inboxes (error_code: channel_access_denied) |
| 404 | "Template not found", "Template not found by id or name for this tenant" | Unknown template ID or name |
| 409 | {"error": "CONTACT_BLOCKED", ...} | The recipient is blocked |
| 422 | list of field errors | Invalid request body, for example no recipient (contact_id, contact_method_id, whatsapp_id or conversation_id) |
Example error:
{
"detail": {
"message": "Missing value for template parameter(s): 1, 3",
"error_code": "missing_template_parameters"
}
}
Next Steps
- Understanding Templates -- template structure, parameter formats, and management
- 24-Hour Messaging Window -- when templates are required
- WhatsApp Interactive Messages -- buttons and lists on WhatsApp