Skip to content

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:

  1. List connections → get the connectionId and phone numbers for your WhatsApp integration
  2. Search templates → find approved templates available for a channel
  3. Send template → fire a template message to a session

Step 1: List Connections

Returns all WhatsApp connections for your account.

http
GET /connections

Request

bash
curl "https://api.maiacompany.io/connections" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response

json
{
  "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:

bash
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.

http
GET /channels/{channelId}/templates/search
Path ParameterDescription
channelIdThe channel ID (find it in Settings → Channels → Channel ID)
Query ParameterTypeDefaultDescription
qstring""Search query (matches template name and body text)
statusstringAPPROVEDTemplate status filter (APPROVED, PENDING, REJECTED)
languagestringFilter by language code (e.g., pt_BR, en_US)
limitnumber20Maximum number of results

Request

bash
curl "https://api.maiacompany.io/channels/CHANNEL_ID/templates/search?q=order&status=APPROVED&limit=10" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response

json
{
  "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.

http
POST /channels/{channelId}/template-messages
Path ParameterDescription
channelIdThe channel ID

Request Parameters

FieldTypeRequiredDescription
sessionIdstringYesThe session/conversation ID (or phone number for new conversations)
templateNamestringYesName of the approved template
languageCodestringYesLanguage code (e.g., pt_BR, en_US)
variablesobjectNoVariable substitutions (see Variable Substitution)
mediaHeaderobjectNoMedia attachment for the template header (see Media Headers)
sessionNamestringNoDisplay name for auto-created sessions. Defaults to sessionId if not provided
agentIdstringNoTarget agent ID — triggers a session transfer if different from current
kanbanIdstringNoTarget 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)

bash
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

bash
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:

bash
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)

bash
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:

bash
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:

json
{
  "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"
      }
    }
  }
}
FieldTypeDescription
successbooleanWhether the template message was sent successfully
messageIdstringWhatsApp message ID from Meta
isNewSessionbooleantrue if a new session was auto-created, false for existing sessions
messageobjectMessage data for immediate UI display

With Agent/Kanban Override

Send a template message and route the session to a specific agent and kanban:

bash
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

  1. Send a template message with agentId and/or kanbanId in the request body
  2. If the session already exists and the provided agent/kanban differs from the current one, a transfer is triggered
  3. A system message is recorded in the chat history documenting the transfer
  4. If the kanban changes, a new CRM record is created on the target kanban

Transfer Scenarios

Params ProvidedBehavior
agentId onlyAgent is set explicitly; kanbanId is resolved from the channel's agent-kanban config
kanbanId onlyKanban is set explicitly; agentId is resolved from the channel's default agent
Both agentId + kanbanIdBoth are set explicitly — full control over the transfer target
NeitherNo 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:

  • componentType is the template component: header, body, or button
  • index is 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 KeyReplacesIn Component
body_11 in BODYBody text
body_22 in BODYBody text
header_11 in HEADERHeader text
button_11 in BUTTONButton URL suffix

Example

Given a template with body text:

Hello {{1}}, your order {{2}} is ready for pickup at {{3}}.

Send variables:

json
{
  "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:

json
{
  "variables": {
    "header_1": "John",
    "body_1": "#12345",
    "body_2": "shipped"
  }
}

Media Headers

Templates with IMAGE, VIDEO, or DOCUMENT headers require a mediaHeader object.

FieldTypeRequiredDescription
typestringYesMedia type: image, video, or document
urlstringYesAny URL accessible by MAIA's servers (public URL, CDN link, or presigned S3 URL)
filenamestringNoFilename 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:

json
{
  "mediaHeader": {
    "type": "image",
    "url": "https://your-domain.com/promo-banner.jpg"
  }
}

Upload via MAIA's File Attachments flow for persistent media in chat history:

  1. Request an upload URL via POST /messages/upload — returns a presigned uploadUrl and fileKey
  2. PUT the file to the returned presigned upload URL
  3. Get a download URL via GET /attachments/{fileKey} — returns a presigned downloadUrl
  4. Pass the download URL as mediaHeader.url in the send template request

Supported Media Types

TypeFormatsMax Size
imageJPEG, PNG5 MB
videoMP416 MB
documentPDF, DOC, DOCX, XLS, XLSX100 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.

MAIA Platform Documentation