WhatsApp Templates
Send WhatsApp template messages programmatically — for re-engaging contacts outside the 24-hour messaging window. This guide covers the full workflow: listing connections, searching approved templates, and sending template messages with variables and media.
When to Use
- Re-engaging a contact after the 24-hour WhatsApp messaging window has closed
- Sending automated notifications (order updates, appointment reminders, shipping alerts)
- Building custom integrations that trigger template messages from external systems
Workflow Overview
Sending a template message requires 3 steps:
- List connections → get the
connectionIdand phone numbers for your WhatsApp integration - Search templates → find approved templates available for a channel
- Send template → fire a template message to a session
Step 1: List Connections
Returns all WhatsApp connections for your account.
GET /connectionsRequest
curl "https://api.maiacompany.io/connections" \
-H "Authorization: Bearer YOUR_API_TOKEN"Response
{
"items": [
{
"id": "conn_abc123",
"name": "My WhatsApp Business",
"connectionType": "whatsapp_custom_app",
"businessAccountId": "123456789",
"tokenStatus": "active",
"phoneNumbers": [
{
"id": "112233445566",
"displayPhoneNumber": "+55 21 99988-7766",
"verifiedName": "My Business",
"qualityRating": "GREEN"
}
],
"webhookUrl": "https://api.maiacompany.io/providers/whatsapp-byot/webhook/conn_abc123",
"createdAt": "2025-06-15T10:30:00.000Z"
}
]
}You can filter by connection type using a query parameter:
curl "https://api.maiacompany.io/connections?type=whatsapp_custom_app" \
-H "Authorization: Bearer YOUR_API_TOKEN"Step 2: Search Templates
Search for approved WhatsApp templates available for a specific channel. Templates are searched via full-text search and filtered by status.
GET /channels/{channelId}/templates/search| Path Parameter | Description |
|---|---|
channelId | The channel ID (find it in Settings → Channels → Channel ID) |
| Query Parameter | Type | Default | Description |
|---|---|---|---|
q | string | "" | Search query (matches template name and body text) |
status | string | APPROVED | Template status filter (APPROVED, PENDING, REJECTED) |
language | string | — | Filter by language code (e.g., pt_BR, en_US) |
limit | number | 20 | Maximum number of results |
Request
curl "https://api.maiacompany.io/channels/CHANNEL_ID/templates/search?q=order&status=APPROVED&limit=10" \
-H "Authorization: Bearer YOUR_API_TOKEN"Response
{
"templates": [
{
"name": "order_confirmation",
"language": "pt_BR",
"category": "UTILITY",
"status": "APPROVED",
"bodyText": "Olá {{1}}, seu pedido {{2}} foi confirmado! Previsão de entrega: {{3}}.",
"headerText": null,
"footerText": "Obrigado por comprar conosco!",
"components": [
{
"type": "BODY",
"text": "Olá {{1}}, seu pedido {{2}} foi confirmado! Previsão de entrega: {{3}}."
},
{
"type": "FOOTER",
"text": "Obrigado por comprar conosco!"
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "URL",
"text": "Track Order",
"url": "https://example.com/track/{{1}}"
}
]
}
]
}
],
"total": 1,
"searchTimeMs": 12
}TIP
Use status=APPROVED (the default) to only see templates that can actually be sent. Templates in PENDING or REJECTED status cannot be sent via the Meta API.
Step 3: Send Template Message
Send a template message to a session. This is used when the 24-hour WhatsApp messaging window has closed and you need to re-engage the contact.
POST /channels/{channelId}/template-messages| Path Parameter | Description |
|---|---|
channelId | The channel ID |
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
sessionId | string | Yes | The session/conversation ID (or phone number for new conversations) |
templateName | string | Yes | Name of the approved template |
languageCode | string | Yes | Language code (e.g., pt_BR, en_US) |
variables | object | No | Variable substitutions (see Variable Substitution) |
mediaHeader | object | No | Media attachment for the template header (see Media Headers) |
sessionName | string | No | Display name for auto-created sessions. Defaults to sessionId if not provided |
agentId | string | No | Target agent ID — triggers a session transfer if different from current |
kanbanId | string | No | Target kanban ID — triggers a session transfer if different from current |
Auto-Session Creation
If the sessionId does not match an existing session, a new session is automatically created. This is useful for initiating new WhatsApp conversations by sending a template to a phone number that has no session yet — just pass the phone number as sessionId. A CRM record is also created automatically for the new session.
Use the optional sessionName field to set a display name for the new session. If omitted, the sessionId (phone number) is used as the name.
Basic Request (No Variables)
curl -X POST "https://api.maiacompany.io/channels/CHANNEL_ID/template-messages" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "SESSION_ID",
"templateName": "welcome_message",
"languageCode": "pt_BR"
}'With Variables
curl -X POST "https://api.maiacompany.io/channels/CHANNEL_ID/template-messages" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "SESSION_ID",
"templateName": "order_confirmation",
"languageCode": "pt_BR",
"variables": {
"body_1": "João",
"body_2": "#12345",
"body_3": "25/01/2025"
}
}'With Media Header (Image)
You can use any accessible URL — either an external public URL or a presigned download URL from the File Attachments upload flow:
curl -X POST "https://api.maiacompany.io/channels/CHANNEL_ID/template-messages" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "SESSION_ID",
"templateName": "promo_with_image",
"languageCode": "pt_BR",
"variables": {
"body_1": "João",
"body_2": "20%"
},
"mediaHeader": {
"type": "image",
"url": "https://your-domain.com/promo-banner.jpg"
}
}'With Media Header (Document)
curl -X POST "https://api.maiacompany.io/channels/CHANNEL_ID/template-messages" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "SESSION_ID",
"templateName": "invoice_template",
"languageCode": "pt_BR",
"variables": {
"body_1": "João",
"body_2": "R$ 150,00"
},
"mediaHeader": {
"type": "document",
"url": "https://your-domain.com/invoice-12345.pdf",
"filename": "invoice-12345.pdf"
}
}'Initiating a New Conversation
To start a new WhatsApp conversation with a contact that has no existing session, pass the phone number as sessionId:
curl -X POST "https://api.maiacompany.io/channels/CHANNEL_ID/template-messages" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "5521999887766",
"templateName": "welcome_message",
"languageCode": "pt_BR",
"sessionName": "João Silva",
"variables": {
"body_1": "João"
}
}'Response
Returns 200 OK on success:
{
"success": true,
"messageId": "wamid.ABGGFlA5FqICkA...",
"isNewSession": false,
"message": {
"id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"sessionId": "SESSION_ID",
"role": "assistant",
"content": "Olá João, seu pedido #12345 foi confirmado! Previsão de entrega: 25/01/2025.",
"timestamp": 1704067200000,
"templateData": {
"templateName": "order_confirmation",
"templateLanguage": "pt_BR",
"variables": {
"body_1": "João",
"body_2": "#12345",
"body_3": "25/01/2025"
}
}
}
}| Field | Type | Description |
|---|---|---|
success | boolean | Whether the template message was sent successfully |
messageId | string | WhatsApp message ID from Meta |
isNewSession | boolean | true if a new session was auto-created, false for existing sessions |
message | object | Message data for immediate UI display |
With Agent/Kanban Override
Send a template message and route the session to a specific agent and kanban:
curl -X POST "https://api.maiacompany.io/channels/CHANNEL_ID/template-messages" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "5521999887766",
"templateName": "welcome_message",
"languageCode": "pt_BR",
"sessionName": "João Silva",
"agentId": "TARGET_AGENT_ID",
"kanbanId": "TARGET_KANBAN_ID",
"variables": {
"body_1": "João"
}
}'Session Transfer via Template
You can trigger an implicit session transfer by including agentId and/or kanbanId in your template message request. When the provided values differ from the session's current agent or kanban, the session is automatically transferred before the template is sent.
How It Works
- Send a template message with
agentIdand/orkanbanIdin the request body - If the session already exists and the provided agent/kanban differs from the current one, a transfer is triggered
- A system message is recorded in the chat history documenting the transfer
- If the kanban changes, a new CRM record is created on the target kanban
Transfer Scenarios
| Params Provided | Behavior |
|---|---|
agentId only | Agent is set explicitly; kanbanId is resolved from the channel's agent-kanban config |
kanbanId only | Kanban is set explicitly; agentId is resolved from the channel's default agent |
Both agentId + kanbanId | Both are set explicitly — full control over the transfer target |
| Neither | No transfer — template is sent via the session's current agent |
TIP
Transfer only triggers on existing sessions. For new sessions (first template message), the agentId and kanbanId simply set the initial assignment.
Channel Configuration Required
The target agent must be in the channel's allowedAgentIds. For automatic kanban resolution, configure agentKanbanConfigs on the channel.
Variable Substitution
Template variables use the format {componentType}_{index}, where:
componentTypeis the template component:header,body, orbuttonindexis the 1-based position of the variable within that component
How It Works
Template text uses 1, 2, etc. as placeholders. The variables object maps these placeholders using the naming convention:
| Variable Key | Replaces | In Component |
|---|---|---|
body_1 | 1 in BODY | Body text |
body_2 | 2 in BODY | Body text |
header_1 | 1 in HEADER | Header text |
button_1 | 1 in BUTTON | Button URL suffix |
Example
Given a template with body text:
Hello {{1}}, your order {{2}} is ready for pickup at {{3}}.Send variables:
{
"variables": {
"body_1": "John",
"body_2": "#12345",
"body_3": "Store Downtown"
}
}Result:
Hello John, your order #12345 is ready for pickup at Store Downtown.Header Variables
If the template has a text header with variables:
Order Update for {{1}}Include a header_1 variable:
{
"variables": {
"header_1": "John",
"body_1": "#12345",
"body_2": "shipped"
}
}Media Headers
Templates with IMAGE, VIDEO, or DOCUMENT headers require a mediaHeader object.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Media type: image, video, or document |
url | string | Yes | Any URL accessible by MAIA's servers (public URL, CDN link, or presigned S3 URL) |
filename | string | No | Filename for document downloads (recommended for document type) |
The url accepts any URL that MAIA's servers can download from — public URLs, CDN links, or presigned S3 URLs all work.
Option A: External URL (simplest)
Pass any publicly accessible URL directly:
{
"mediaHeader": {
"type": "image",
"url": "https://your-domain.com/promo-banner.jpg"
}
}Option B: File Attachments Upload Flow (recommended)
Upload via MAIA's File Attachments flow for persistent media in chat history:
- Request an upload URL via
POST /messages/upload— returns a presigneduploadUrlandfileKey - PUT the file to the returned presigned upload URL
- Get a download URL via
GET /attachments/{fileKey}— returns a presigneddownloadUrl - Pass the download URL as
mediaHeader.urlin the send template request
Supported Media Types
| Type | Formats | Max Size |
|---|---|---|
image | JPEG, PNG | 5 MB |
video | MP4 | 16 MB |
document | PDF, DOC, DOCX, XLS, XLSX | 100 MB |
How media upload works
MAIA downloads the file from the provided URL and re-uploads it to Meta's servers automatically. You do not need the URL to be publicly accessible by Meta — it only needs to be reachable by MAIA's servers. Using the File Attachments upload flow ensures media is stored and viewable in the chat history.
Templates & Meta Approval
Templates must be APPROVED by Meta before they can be sent. Use the search endpoint with status=APPROVED to only see sendable templates. Template messages are sent via the Meta API and may incur WhatsApp Business charges per your Meta billing agreement.