# Message templates

Create WhatsApp templates, follow Meta's review, fill variables, media headers and buttons, and send a template to start a conversation.

A template is a pre-approved message. It is the only thing you can send to someone who has not messaged you in the last 24 hours, so every outbound campaign, reminder and one-time code on WhatsApp starts with one. You create the template, Meta reviews it, and once it is `APPROVED` you can send it as often as you like with different values filled in.

## Create a template

Send [`POST /v1/whatsapp/templates`](https://pozy.ai/docs/api/createWhatsAppTemplate) with the account, a `name`, a `category`, a `language` and the `components` that make up the message.

```bash cURL
curl -X POST https://api.pozy.ai/v1/whatsapp/templates \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "6712a9d4c0e3b58f1a7d2e64",
    "name": "pickup_ready",
    "category": "UTILITY",
    "language": "en",
    "components": [
      {
        "type": "body",
        "text": "Hi {{1}}, your order {{2}} is ready for pickup until {{3}}.",
        "example": { "body_text": [["Marta", "#4821", "6pm"]] }
      },
      { "type": "footer", "text": "Reply STOP to opt out" }
    ]
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/whatsapp/templates", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    accountId: "6712a9d4c0e3b58f1a7d2e64",
    name: "pickup_ready",
    category: "UTILITY",
    language: "en",
    components: [
      {
        type: "body",
        text: "Hi {{1}}, your order {{2}} is ready for pickup until {{3}}.",
        example: { body_text: [["Marta", "#4821", "6pm"]] },
      },
      { type: "footer", text: "Reply STOP to opt out" },
    ],
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);

console.log(data.template.id, data.template.status); // "PENDING"
```
```python Python
import os
import requests

resp = requests.post(
    "https://api.pozy.ai/v1/whatsapp/templates",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "accountId": "6712a9d4c0e3b58f1a7d2e64",
        "name": "pickup_ready",
        "category": "UTILITY",
        "language": "en",
        "components": [
            {
                "type": "body",
                "text": "Hi {{1}}, your order {{2}} is ready for pickup until {{3}}.",
                "example": {"body_text": [["Marta", "#4821", "6pm"]]},
            },
            {"type": "footer", "text": "Reply STOP to opt out"},
        ],
    },
    timeout=30,
)
resp.raise_for_status()
print(resp.json()["template"])
```

```json
{
  "success": true,
  "template": {
    "id": "781203456789012",
    "name": "pickup_ready",
    "status": "PENDING",
    "category": "UTILITY",
    "language": "en"
  }
}
```

Rules for the request:

- `name` uses lowercase letters, digits and underscores and starts with a letter.
- Meta stores one template per name and language. The same `name` in three languages is three templates, each with its own `id`.
- Every variable needs an `example` value. Reviewers reject templates they cannot read with real values.

## Pick the category

The category decides what Meta charges per delivery and how long Meta keeps trying to deliver. Pick the one that matches the content honestly; Meta reclassifies templates that are filed under the wrong one.

| `category` | Use it for | Delivery window you can set | Meta's default |
| --- | --- | --- | --- |
| `UTILITY` | Order updates, receipts, reminders, account notices | 30 seconds to 12 hours | 30 days |
| `MARKETING` | Promotions, offers, newsletters, re-engagement | 12 hours to 30 days | 30 days |
| `AUTHENTICATION` | One-time passcodes | 30 seconds to 15 minutes | 10 minutes |

Set the window with `message_send_ttl_seconds`. A message not delivered inside it is dropped rather than delivered hours afterwards. That is right for a passcode and wrong for a promotion. On create you can pass `-1` to keep the 30-day default for `UTILITY` and `AUTHENTICATION`.

When Meta changes a category it sends `whatsapp.template.category_updated` twice: once with `changeType: "scheduled"` as a 24-hour notice, and again with `changeType: "applied"` when it takes effect. Meta clears any custom delivery window at that point, so read it back afterwards.

## Build the components

A template is a list of components. Only `body` is required.

| Component | What it holds | Notes |
| --- | --- | --- |
| `header` | `format` is `text`, `image`, `video`, `gif`, `document` or `location` | A text header can carry one variable. A media header needs an `example.header_handle` for review and real media at send time. |
| `body` | The main text with variables | `add_security_recommendation` adds Meta's standard warning on authentication templates. |
| `footer` | Short plain text | `code_expiration_minutes` on authentication templates. |
| `buttons` | Quick replies, links, phone, code and flow buttons | Types: `quick_reply`, `url`, `phone_number`, `otp`, `copy_code`, `flow`, `mpm`, `catalog`. |
| `carousel` | Up to 10 cards, each with its own header, body and buttons | Every card must use the same structure. |
| `limited_time_offer` | Offer text (16 characters) and an optional expiry | Marketing templates only. |

### Variables

Variables are positional by default: `{{1}}`, `{{2}}` and so on. To use names, set `parameter_format: "NAMED"` and write `{{customer_name}}` in the text, with the examples in `body_text_named_params`.

[Broadcasts](https://pozy.ai/docs/whatsapp/broadcasts#fill-variables-per-recipient) only fill positional variables. Use positional ones if you plan to broadcast the template.

### Buttons

```json
{
  "type": "buttons",
  "buttons": [
    { "type": "quick_reply", "text": "Change pickup time" },
    { "type": "url", "text": "View order", "url": "https://example.com/orders/{{1}}", "example": ["4821"] },
    { "type": "phone_number", "text": "Call the shop", "phone_number": "+351210000123" }
  ]
}
```

A `url` button can end in one variable, filled at send time. A `flow` button opens a [WhatsApp Flow](https://pozy.ai/docs/whatsapp/flows) by `flow_id` or `flow_name`.

## Wait for approval

Review can take up to 24 hours. Subscribe to `whatsapp.template.status_updated` instead of polling. It fires whenever Meta finishes a review or re-review and carries the new `status` and Meta's `reason` (`"NONE"` on approval).

| `status` | Meaning | Can you send it? |
| --- | --- | --- |
| `PENDING` | In review | No |
| `APPROVED` | Live | Yes |
| `REJECTED` | Declined. Fix the content and create it again, or appeal in WhatsApp Manager. | No |
| `IN_APPEAL` | An appeal is being reviewed | No |
| `PAUSED` | Meta paused it after poor recipient feedback | No |
| `DISABLED` | Switched off by Meta | No |
| `PENDING_DELETION` | Delete requested; removed after 24 hours | No |

To check by hand, list templates with [`GET /v1/whatsapp/templates`](https://pozy.ai/docs/api/getWhatsAppTemplates) and filter by `status`, `name` or `language`. The list is read live from Meta.

> **Skip the review with Meta's library:**
> 
> Meta keeps a library of pre-approved utility templates. Look one up with [`GET /v1/whatsapp/template-library`](https://pozy.ai/docs/api/getWhatsAppLibraryTemplate), then create it with `library_template_name` instead of `components`. It comes back `APPROVED` immediately. If the library template has URL or phone buttons, send one `library_template_button_inputs` entry per button, in order, or Meta rejects it.

## Send a template

To open a conversation, call [`POST /v1/inbox/conversations`](https://pozy.ai/docs/api/createInboxConversation). The recipient goes in `participantId` as digits with the country code and no `+`. `templateParams` is one flat list in the order the variables appear: header variable first, then body variables.

```bash cURL
curl -X POST https://api.pozy.ai/v1/inbox/conversations \
  -H "Authorization: Bearer $POZY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "accountId": "6712a9d4c0e3b58f1a7d2e64",
    "participantId": "351912345678",
    "templateName": "pickup_ready",
    "templateLanguage": "en",
    "templateParams": ["Marta", "#4821", "6pm"]
  }'
```
```ts Node
const res = await fetch("https://api.pozy.ai/v1/inbox/conversations", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.POZY_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    accountId: "6712a9d4c0e3b58f1a7d2e64",
    participantId: "351912345678",
    templateName: "pickup_ready",
    templateLanguage: "en",
    templateParams: ["Marta", "#4821", "6pm"],
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);

console.log(data.data.conversationId, data.data.messageId);
```
```python Python
resp = requests.post(
    "https://api.pozy.ai/v1/inbox/conversations",
    headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
    json={
        "accountId": "6712a9d4c0e3b58f1a7d2e64",
        "participantId": "351912345678",
        "templateName": "pickup_ready",
        "templateLanguage": "en",
        "templateParams": ["Marta", "#4821", "6pm"],
    },
    timeout=30,
)
print(resp.json()["data"])
```

Other parts of the template are filled with their own fields on the same call:

| Field | Fills |
| --- | --- |
| `headerMedia` | The image, video or document of a media header (`type` plus a public `link` or an uploaded media `id`) |
| `headerLocation` | A location header (`latitude`, `longitude`, optional `name` and `address`) |
| `templateButtonParams` | Button variables by `index`, with `subType` `url`, `copy_code` or `flow` |
| `templateCards` | Per-card values for a carousel (`cardIndex`, `params`, `headerMedia`, `buttons`) |

`templateLanguage` must match exactly. `en` and `en_US` are two different templates.

Inside an existing conversation, send a template with [`POST /v1/inbox/conversations/{conversationId}/messages`](https://pozy.ai/docs/api/sendInboxMessage). To send the same template to many people with per-person values, use a [broadcast](https://pozy.ai/docs/whatsapp/broadcasts).

## Edit or delete a template

Update by name with [`PATCH /v1/whatsapp/templates/{templateName}`](https://pozy.ai/docs/api/updateWhatsAppTemplate) or by Meta id with [`PATCH /v1/whatsapp/templates/id/{templateId}`](https://pozy.ai/docs/api/updateWhatsAppTemplateById). Put `language` in the body to say which variant you mean. A name that exists in several languages without `language` returns `409` with code `ambiguous_template`.

- Changing `components` sends the template back to review and returns `status: "PENDING"`.
- Changing only `message_send_ttl_seconds` keeps an approved template approved. `-1` is not allowed here.
- Meta allows one edit per 24 hours and 10 per 30 days, and only from `APPROVED`, `REJECTED` or `PAUSED`.

Deleting moves a template to `PENDING_DELETION` for 24 hours before Meta removes it.

## Common errors

| Error | Cause | Fix |
| --- | --- | --- |
| `400` on create | Name format, missing example or a category outside the three values | Check the rules above and resend |
| `400` with `param: message_send_ttl_seconds` | Window outside the range for the category | Use a value from the category table |
| Meta code `132001`, template not found | Name or language code does not match | Use the exact name and language the template was created with |
| Meta code `132000`, parameter count | Fewer or more `templateParams` than variables | Send one value per variable, in order |
| `REJECTED` status | Review failed; not an API error | Read `reason` on `whatsapp.template.status_updated`, fix the text, create it again |

## Endpoints

| Endpoint | What it does |
| --- | --- |
| [`GET /v1/whatsapp/templates`](https://pozy.ai/docs/api/getWhatsAppTemplates) | List templates |
| [`POST /v1/whatsapp/templates`](https://pozy.ai/docs/api/createWhatsAppTemplate) | Create template |
| [`GET /v1/whatsapp/templates/{templateName}`](https://pozy.ai/docs/api/getWhatsAppTemplate) | Get template |
| [`PATCH /v1/whatsapp/templates/{templateName}`](https://pozy.ai/docs/api/updateWhatsAppTemplate) | Update template |
| [`DELETE /v1/whatsapp/templates/{templateName}`](https://pozy.ai/docs/api/deleteWhatsAppTemplate) | Delete template |
| [`GET /v1/whatsapp/templates/id/{templateId}`](https://pozy.ai/docs/api/getWhatsAppTemplateById) | Get template by id |
| [`PATCH /v1/whatsapp/templates/id/{templateId}`](https://pozy.ai/docs/api/updateWhatsAppTemplateById) | Update template by id |
| [`DELETE /v1/whatsapp/templates/id/{templateId}`](https://pozy.ai/docs/api/deleteWhatsAppTemplateById) | Delete template by id |
| [`GET /v1/whatsapp/template-library`](https://pozy.ai/docs/api/getWhatsAppLibraryTemplate) | Look up a library template |
| [`POST /v1/inbox/conversations`](https://pozy.ai/docs/api/createInboxConversation) | Create conversation |
| [`POST /v1/inbox/conversations/{conversationId}/messages`](https://pozy.ai/docs/api/sendInboxMessage) | Send message |

## Webhook events

| Event | When it fires |
| --- | --- |
| [`whatsapp.template.status_updated`](https://pozy.ai/docs/webhooks/events#whatsapp-template-status-updated) | WhatsApp template status updated. |
| [`whatsapp.template.category_updated`](https://pozy.ai/docs/webhooks/events#whatsapp-template-category-updated) | WhatsApp template category updated. |
