> ## Documentation Index
> Fetch the complete documentation index at: https://docs.callkaro.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Send WhatsApp Template

> Send an approved WhatsApp template and record it in your inbox. The recipient is created as a contact automatically if they are not one already, with lead direction 'outbound'.

<Note>
  Sends an approved WhatsApp template to one recipient and records it in your Inbox, so the send appears in the conversation alongside everything your agent does.
</Note>

## Quick Example

```bash theme={null}
curl -X POST "https://api.callkaro.ai/whatsapp/send-template" \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: YOUR_API_KEY" \
  -d '{
    "phone_number_id": "123456789012345",
    "to_number": "919876543210",
    "template_name": "order_shipped",
    "components": [
      { "type": "body", "parameters": [
        { "type": "text", "text": "Rahul" },
        { "type": "text", "text": "#1234" }
      ]}
    ],
    "metadata": {
      "plan_type": "Gold",
      "City": "Delhi"
    }
  }'
```

## Use Cases

* **Transactional notifications**: order confirmations, shipping updates, appointment reminders
* **Re-opening a conversation**: outside the 24-hour customer service window a template is the only thing WhatsApp allows
* **Campaign attribution**: pass `wa_campaign_id` so the send, its delivery status and any follow-ups all roll up to one campaign
* **Enriching your CRM as you send**: pass `metadata` and the data you already have about this person is saved onto their contact, instead of being discarded after the send

## What happens on your account

<Steps>
  <Step title="Ownership is verified">
    The `phone_number_id` is checked against your WhatsApp Business Account. A number that is not yours returns `403`, so a valid API key can never send from another customer's number.
  </Step>

  <Step title="The language is resolved">
    If you omit `language_code`, the language the template is actually approved in is used. Pass it explicitly to override.
  </Step>

  <Step title="The message is recorded">
    The rendered template is stored and appears in your Inbox. A rejected send is stored too, with Meta's reason attached, rather than disappearing into the API response.
  </Step>

  <Step title="A contact is created if needed">
    If this number is not yet a contact, one is created with lead direction `outbound`. An existing contact is never reclassified.
  </Step>

  <Step title="metadata is saved onto that contact">
    Any `metadata` keys matching your contact attributes are written in the same operation, so one call sends the template and populates the contact.
  </Step>
</Steps>

## Saving contact attributes with `metadata`

The data you already have about a person — their plan, their city, an order number — is usually worth keeping after the send. Pass it as `metadata` and it is saved onto that recipient's CRM contact, exactly the way extra columns in a campaign CSV are.

```json theme={null}
{
  "phone_number_id": "123456789012345",
  "to_number": "919876543210",
  "template_name": "order_shipped",
  "metadata": {
    "plan_type": "Gold",
    "City": "Delhi"
  }
}
```

The response tells you what happened:

```json theme={null}
{
  "status": "success",
  "message": "Template sent",
  "message_id": "wamid.HBgMOTE5ODc2NTQzMjEw",
  "contact_updated": ["City", "plan_type"],
  "metadata_ignored": []
}
```

| Rule                             | Behaviour                                                                                                                                                                              |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The attribute must already exist | Create it under **Contacts → Attributes** first, then use its name as the key. Keys matching no attribute are ignored and returned in `metadata_ignored`                               |
| Name matching is forgiving       | Case, spaces, underscores and hyphens are ignored — `"Plan Type"`, `"plan_type"` and `"plantype"` all match the same attribute. The value is stored under the attribute's own spelling |
| Blank values are skipped         | Sending `""` or `null` leaves the stored value alone, so a partly-filled payload never wipes existing data                                                                             |
| Hand-edited values are protected | An attribute someone edited by hand in the CRM is never overwritten by this API                                                                                                        |
| Other attributes are untouched   | Only the keys you send are written; everything else on the contact is left as-is                                                                                                       |
| Failed sends save nothing        | If Meta rejects the send you get a `400` and no contact is created or updated. Retry the whole call                                                                                    |

**Limits:** at most 50 keys, each value at most 1000 characters. Both are rejected with a `400` rather than truncated — silently shortening a value would put corrupted data in your CRM with no way to tell.

<Note>
  **`metadata` vs `components`.** `components` fills the template's variables for this one message. `metadata` is saved on the contact and persists. They are separate on purpose: a value in `components` is not stored on the contact, and `metadata` is never sent to WhatsApp. Send a value in both if you want it to do both.
</Note>

<Warning>
  Check `metadata_ignored` the first time you integrate. A mistyped or undefined attribute name does **not** fail the request — the message still goes out and the response lists what was dropped. If you never look, a typo surfaces much later as an empty column, with the sends already made.
</Warning>

## Delivery status

The response `message_id` is WhatsApp's `wamid`. Delivery is asynchronous, and a `200` here does **not** mean the user received the message.

| Status                | Reached the user?                                                      |
| --------------------- | ---------------------------------------------------------------------- |
| `sent`                | **Not yet** — WhatsApp accepted it, but it is not on the user's device |
| `delivered`           | **Yes** — on their device                                              |
| `read`                | **Yes** — opened                                                       |
| `failed` / `not sent` | **No**                                                                 |

A message starts at `sent` and only `delivered` or `read` confirms it arrived. To read the current status, use [Get WhatsApp Messages](/api-reference/whatsapp-messages), or subscribe to the `message_status_updated` [webhook event](/webhook/introduction).

## Errors worth handling

<AccordionGroup>
  <Accordion title="400 with a whatsapp_error object">
    Meta rejected the send. The `message` field carries Meta's own reason, which is specific — a wrong template name, a variable count that does not match the approved template, or a field over its length limit (footer max 60 characters, body max 1024). Fix the payload rather than retrying.
  </Accordion>

  <Accordion title="403 phone_number_id does not belong to this account">
    The number is not on your WABA. Check the ID in **WhatsApp → Phone Numbers**.
  </Accordion>

  <Accordion title="400 WhatsApp Business Account not linked">
    The account has no `wabaId`. Complete WhatsApp onboarding first.
  </Accordion>

  <Accordion title="400 metadata must be an object / too many keys / value too long">
    `metadata` has to be a JSON object of `{attribute_name: value}`, with at most 50 keys and values of at most 1000 characters. Note that an *undefined* attribute name is not an error — it is ignored and reported in `metadata_ignored`.
  </Accordion>
</AccordionGroup>

<Warning>
  A rejected send is **not** retried for you, and a `400` will keep failing with the same payload. Only `5xx` responses are worth retrying.
</Warning>


## OpenAPI

````yaml POST /whatsapp/send-template
openapi: 3.1.0
info:
  title: CallKaro AI API
  description: Campaign and outbound call management API for CallKaro AI voice agents
  version: 1.0.0
  contact:
    name: CallKaro AI Support
    email: support@callkaro.ai
    url: https://callkaro.ai
servers:
  - url: https://api.callkaro.ai
    description: Production server
security:
  - ApiKeyAuth: []
paths:
  /whatsapp/send-template:
    post:
      tags:
        - Endpoints
      summary: Send WhatsApp Template
      description: >-
        Send an approved WhatsApp template and record it in your inbox. The
        recipient is created as a contact automatically if they are not one
        already, with lead direction 'outbound'.
      operationId: sendWhatsAppTemplate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WhatsAppSendTemplateRequest'
            example:
              phone_number_id: '123456789012345'
              to_number: '919876543210'
              template_name: order_shipped
              language_code: en
              components:
                - type: body
                  parameters:
                    - type: text
                      text: Rahul
                    - type: text
                      text: '#1234'
              wa_campaign_id: spring_promo_2026
              metadata:
                plan_type: Gold
                City: Delhi
      responses:
        '200':
          description: Template sent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WhatsAppSendTemplateResponse'
              example:
                status: success
                message: Template sent
                message_id: wamid.HBgMOTE5...
        '400':
          description: Invalid request, or WhatsApp rejected the send
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                status: error
                message: 'Footer text length invalid. Min length: 0, Max length: 60'
                whatsapp_error:
                  code: 400
                  message: ...
        '401':
          description: Missing or invalid X-API-KEY
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                status: error
                message: Invalid API key
        '403':
          description: phone_number_id does not belong to this account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                status: error
                message: phone_number_id does not belong to this account
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      security:
        - ApiKeyAuth: []
components:
  schemas:
    WhatsAppSendTemplateRequest:
      type: object
      required:
        - phone_number_id
        - to_number
        - template_name
      properties:
        phone_number_id:
          type: string
          description: >-
            WhatsApp phone number ID you are sending from. Must belong to your
            WABA.
        to_number:
          type: string
          description: Recipient, digits only including country code.
        template_name:
          type: string
          description: Name of an approved WhatsApp template.
        language_code:
          type: string
          default: en
          description: >-
            Defaults to the language the template is actually approved in when
            that can be resolved, otherwise "en".
        components:
          type: array
          items:
            type: object
          description: >-
            Meta template components (variable values). Same shape Meta's own
            send API takes.
          default: []
        agent_id:
          type: string
          description: Optional. Tags the stored message with one of your agents.
        wa_campaign_id:
          type: string
          description: >-
            Optional campaign to attribute this send to. 1-64 characters of
            letters, digits, '_' or '-'. Alias: waCampaignId. Follow-ups
            triggered by this message inherit it.
        metadata:
          type: object
          additionalProperties:
            type: string
          description: >-
            Optional. Contact attributes to save onto the recipient's CRM
            contact, as {attribute_name: value}. The attribute must already be
            defined on your account; keys matching no definition are ignored and
            listed in metadata_ignored. Name matching ignores case, spaces,
            underscores and hyphens. Blank values are skipped, and a value
            edited by hand in the CRM is never overwritten. Max 50 keys, each
            value max 1000 characters. Nothing is written if the send fails.
          example:
            plan_type: Gold
            City: Delhi
        extra:
          type: object
          description: >-
            Optional. Stored verbatim on the message row. Unlike metadata, this
            is not written to the contact.
    WhatsAppSendTemplateResponse:
      type: object
      properties:
        status:
          type: string
          example: success
        message:
          type: string
          example: Template sent
        message_id:
          type: string
          description: WhatsApp message ID (wamid) of the sent message.
        contact_updated:
          type: array
          items:
            type: string
          description: Attribute names from metadata that were saved onto the contact.
          example:
            - City
            - plan_type
        metadata_ignored:
          type: array
          items:
            type: string
          description: >-
            metadata keys that matched no attribute definition and were dropped.
            The send still succeeded -- check this on first integration to catch
            a mistyped name.
          example: []
    ErrorResponse:
      type: object
      properties:
        status:
          type: string
          example: error
        message:
          type: string
          description: Error description
          example: 'Missing required field: to_number'
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-KEY
      description: API key from https://callkaro.ai/dashboard/api-key

````