Skip to main content

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​

ScopePurpose
messages:createSend 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​

FieldTypeRequiredDescription
contact_idstringOne recipient field required¹The contact to send to. Uses the contact's first WhatsApp contact method, falling back to its phone number.
contact_method_idstringOne 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_idstringOne 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_idstringOne recipient field required¹Send within an existing conversation (must be a WhatsApp conversation). Also disambiguates a template name shared across channels/languages.
channel_idstringNoThe WhatsApp channel to send from. Optional — see Choosing the channel.
languagestringNoThe 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_valuesstring[] | objectNoValues for the body placeholders. Shape depends on the template's parameter_format (see below). An empty value counts as not set.
variable_mappingobjectNoContact fields to fill variables from, for this send only. Keyed like variable_values ("1" or "first_name"). See Mapping variables to contact fields.
variable_fallbacksobjectNoFixed text per variable, used when the variable would otherwise be empty. Keyed like variable_values.
header_media_urlstringNoHeader 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_filenamestringNoDOCUMENT 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.
cardsobject[]NoCAROUSEL 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.

FieldUse when…Contact handling
contact_idYou 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_idA contact has multiple WhatsApp identifiers and you must target a specific one.Pins the exact method and its owning contact.
whatsapp_idYou 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_idYou'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:

  1. contact_method_id — the method's value is the recipient and its owning contact is used. The method must be of type whatsapp_id or whatsapp_bsuid (any other type → 400). If contact_id is also sent it must name the method's owning contact — a mismatch returns 400 (it is no longer silently ignored).
  2. conversation_id — the conversation must belong to a WhatsApp channel (400 otherwise). The recipient and contact are taken from the conversation.
  3. 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_id is 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_bsuid method scoped to the sending channel; a phone-shaped or alphanumeric id becomes a workspace-wide whatsapp_id method.
  4. contact_id — uses the contact's first whatsapp_id method, then its first whatsapp_bsuid method, 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_id of the channel it belongs to when sending by a bare whatsapp_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:

  1. 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 returns 400 with error_code template_channel_mismatch and nothing is sent.
  2. else conversation_id — sent from the conversation's channel. That channel must be in the template's account.
  3. 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 language that differs from the row's language, the request returns 400. (Reference the template by name to choose a language.)
  • By name, only one language exists — that language is used; language is optional.
  • By name, multiple languages exist:
    1. if you pass language, that exact variant is used — 400 if the name has no variant in that language (the error lists the available languages);
    2. otherwise SendSeven auto-resolves from the recipient contact's preferred language (Contact.language), matching the code exactly first, then by base-language family (so de matches de_DE, and pt_BR matches pt);
    3. if there is no language and the contact has no language (or none that matches an available variant), the request returns 400 asking you to specify language or set the contact's language.
note

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:

  1. header_document_filename in the request (per-send override), else
  2. 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
  3. the original filename of the uploaded attachment, when header_media_url resolves to a file uploaded through SendSeven (attachment ID, proxy/public URL, or GCS ID), else
  4. nothing — the field is omitted and WhatsApp decides what to display. This is the case for external https URLs with no stored filename.
caution

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.

Carousels have no document headers

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"
}
}
tip

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.

A carousel template has a top-level BODY and up to 10 cards. Fill them with two things:

  • the top-level variable_values for the carousel's own body text, exactly as above;
  • a cards array 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:

FieldTypeDescription
variable_valuesstring[] | objectValues 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_urlstringMedia 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:

MappingFills the variable with
contact.first_name, contact.last_name, contact.nameThe contact's name (or part of it)
contact.email, contact.phone, contact.birthdayThe 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.nameYour workspace name
agent.name, agent.nicknameThe 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:

  1. a non-empty variable_values entry
  2. variable_mapping from the request
  3. the mapping saved on the template (Settings > WhatsApp templates)
  4. 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:

TriggerResponse 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_fallbacksMissing 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
}
FieldTypeDescription
successbooleantrue when the template was accepted by WhatsApp
message_idstring | nullInternal SendSeven message ID
external_idstring | nullWhatsApp message ID (wamid…)
conversation_idstringConversation the message was sent in (new or existing)
errorstring | nullHuman-readable error when success is false
error_codeinteger | nullWhatsApp API error code (e.g. 131008 missing parameters, 131042 payment issue)
error_detailsstring | nullAdditional detail from Meta (e.g. which parameter is missing)
note

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.

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 detail as a message string (for example a wrong variable_values shape).
  • Send failures return 400 with an object: {"detail": {"message": "...", "error_code": ..., "error_details": "..."}}. error_code is present only when there is a code: the string missing_template_parameters, or WhatsApp's numeric error code (for example 131008 missing parameters, 132001 template does not exist). When WhatsApp's code is 138000, detail.is_coexistence tells you whether the number is connected through the WhatsApp Business app.
  • Schema errors return 422 with detail as a list of field errors.
StatusdetailDescription
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
422list of field errorsInvalid 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​