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). |
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
The message is always sent from the template's own channel — every template belongs to one channel. channel_id is optional and is used to:
- Disambiguate a template name that exists on more than one channel. When you reference a template by name:
- if
channel_idis given, that channel's copy is used; - else, if
conversation_idis given, the conversation's channel is used; - else, the first matching channel is used. Channel ambiguity never raises an error. (Language ambiguity within the chosen channel is handled separately — see below.)
- if
- Select the sending channel when sending by
whatsapp_idwith no contact or conversation.
If you pass a channel_id that conflicts with the resolved template's own channel (e.g. you reference a template by ID that lives on a different channel, or by a name that doesn't exist on the given channel), the request returns 400.
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)
When you reference a template by name and that name exists on multiple WhatsApp channels, add channel_id to choose which channel sends it. 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 fixed 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; …").
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 is fixed at creation time and never changes, so you can cache it alongside the template ID.
Error cases
If you send the wrong shape for a template's parameter_format, the API returns 400 VALIDATION_ERROR with one of these messages:
| 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', ... |
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.
Variable Mapping
SendSeven supports automatic variable mappings that resolve to real values at send time. The mapping keys below can appear as values inside variable_values (in either list or dict form) and are replaced server-side before the message is dispatched to Meta:
| Mapping Key | Resolves To |
|---|---|
contact.name | Contact's display name |
contact.phone | Contact's phone number |
contact.email | Contact's email address |
agent.name | Assigned agent's full name |
agent.nickname | Assigned agent's nickname |
tenant.name | Your organization name |
Example with Auto-Mapping (positional)
{
"contact_id": "contact_xyz789",
"channel_id": "channel_wa_001",
"language": "en_US",
"variable_values": ["contact.name", "ORD-5678", "Feb 15, 2026"]
}
channel_id and language are optional here — included only to show they may accompany variable_values. See Choosing the channel and Choosing the language.
Example with Auto-Mapping (named)
{
"contact_id": "contact_xyz789",
"channel_id": "channel_wa_001",
"language": "en_US",
"variable_values": {
"name": "contact.name",
"quote": "ORD-5678",
"due_date": "Feb 15, 2026"
}
}
In both examples contact.name is automatically resolved to the contact's actual name (e.g., "John Smith") before sending.
Auto-mapping is useful for personalization at scale -- you do not need to look up contact details before sending templates.
Carousel cards
The mapping keys above work inside each card's variable_values too, following the same positional/named rules per card. So a card can auto-map just like the top-level body:
{
"contact_id": "contact_xyz789",
"variable_values": ["contact.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" }
]
}
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.
Error Responses
| Status | Error Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Missing variables, wrong variable_values shape for the template's parameter_format, invalid language, or template not approved |
| 400 | VALIDATION_ERROR | contact_method_id does not reference a whatsapp_id or whatsapp_bsuid method |
| 400 | VALIDATION_ERROR | An authentication template was addressed to a Business-scoped user ID (whatsapp_bsuid) — WhatsApp requires a phone number for one-tap, zero-tap and copy-code buttons |
| 400 | VALIDATION_ERROR | whatsapp_id is not a valid WhatsApp id or Business-scoped user ID |
| 400 | VALIDATION_ERROR | contact_method_id and contact_id name different contacts |
| 400 | VALIDATION_ERROR | conversation_id does not reference a WhatsApp conversation |
| 400 | VALIDATION_ERROR | channel_id conflicts with the resolved template's own channel (or the named template does not exist on that channel) |
| 400 | VALIDATION_ERROR | language does not match the template referenced by ID, or the named template has no variant in the requested language |
| 400 | VALIDATION_ERROR | Template name exists in multiple languages and no language (or matching contact language) was provided |
| 401 | INVALID_TOKEN | Missing or invalid authentication token |
| 403 | INSUFFICIENT_SCOPE | Token lacks required scope |
| 404 | RESOURCE_NOT_FOUND | Template, contact, contact method, or conversation not found |
| 422 | VALIDATION_ERROR | No recipient supplied — provide one of contact_id, contact_method_id, whatsapp_id, or conversation_id |
| 422 | VALIDATION_ERROR | Resolved recipient has no WhatsApp id or phone number for delivery |
Example error:
{
"detail": "Template uses named parameters; pass values as an object keyed by parameter name",
"error_code": "VALIDATION_ERROR"
}
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