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. 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 +) or an alphanumeric WhatsApp id, 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. |
¹ 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, falling back to its phone number. |
contact_method_id | A contact has multiple WhatsApp numbers 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 (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_id(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):- If
contact_idis also sent, that contact is used and gains awhatsapp_idcontact 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 +
whatsapp_idmethod are created.
- If
contact_id— uses the contact's firstwhatsapp_idmethod, falling back to its phone number.
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.
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 number and you must target one exactly. The method must be of type 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_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.
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', ... |
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.
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 method |
| 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