# WhatsApp templates through the API

Outside the 24-hour window, the only thing WhatsApp lets through is a template
approved by Meta. This guide is about creating them from your code, not from the panel.

## First: a template does NOT belong to the number

This is the misunderstanding that costs the most, so it goes first.

A template belongs to the **WhatsApp account** — the WABA — not to a phone number. If
your account has three numbers, the template you create is available to all three.
There is no such thing as "a template for this number".

And if you have **several accounts** connected to the same channel, then it does
matter which one you create it in: a template from account A cannot be sent from a
number on account B.

## The channel id

Everything starts with the channel:

```ts
const channels = await met.channels.list();
const whatsapp = channels.find((c) => c.type === 'whatsapp')!;
```

And the templates you already have, each with the account it came from:

```ts
const { data, accounts } = await met.channels.templates(whatsapp.id);
```

> **Always look at `accounts`.** If one of your accounts didn't respond, the list
> arrives incomplete and with `200`, not with an error. That's also where the
> `waba_id` values and their aliases come from — which is what you need next.

## Creating a template

```ts
const created = await met.channels.createTemplate(whatsapp.id, {
  name: 'appointment_reminder',
  language: 'en',
  category: 'UTILITY',
  components: [
    { type: 'BODY', text: 'Hi {{1}}, a reminder about your appointment on {{2}}.' },
  ],
  waba_id: '102938475610293',   // optional: omit it and it goes to your primary account
});

created.template_status;   // 'PENDING'
```

It requires the `channels:manage` scope, and **a test key is not enough**: this sends
a real review request to Meta against your client's account.

### If the header carries an image

Meta requires a sample. Pass it as `sample_url` inside the `HEADER` component:

```ts
components: [
  {
    type: 'HEADER',
    format: 'IMAGE',
    sample_url: 'https://your-cdn.com/samples/invoice.png',
  },
  { type: 'BODY', text: 'Your {{1}} invoice is ready.' },
]
```

It must be **`https`** and reachable from the internet: our server downloads it to
upload it to Meta, and for security it rejects internal and private-network
addresses. Maximum 5 MB.

## There is a cap, and it's worth understanding why

**25 creations per account per hour.** Past that you get a `429` with `rate_limited`.

The reason isn't protecting our servers: **a rejected template lowers the quality
rating of the WhatsApp account**. If you are integrating for a client, that rating is
theirs. A script that tries twenty variants of a template until one passes leaves the
damage with someone who had no say in the decision.

## Finding out when Meta decides

`createTemplate` returns immediately with `PENDING`. Meta decides later, and it
usually takes a while. You can ask for the list again, or subscribe and be told:

```ts
await met.webhooks.subscriptions.create({
  url: 'https://your-server.com/webhooks/met',
  enabled_events: [
    'channel.template.approved',
    'channel.template.rejected',
    'channel.template.status_updated',
  ],
});
```

| Event | When | What to do |
|---|---|---|
| `channel.template.approved` | Meta approved it | You can send it now. |
| `channel.template.rejected` | Meta rejected it | Carries `reason`. Fix it and create it again under another name. |
| `channel.template.status_updated` | It changed state for another reason | **Read this one.** See below. |

**The third one is the one not to ignore.** Meta sends fourteen different states over
that channel, and several — paused, disabled, flagged, limit exceeded — happen to
templates that were **already approved and working**. Operationally that is worse than
a rejection: you find a rejection when you go and look, but a pause cuts off sends
that are already running, with nobody touching anything.

The event's `data` carries `template_name`, `template_language`, `waba_id`, Meta's raw
`status` and, when there is one, the `reason`.

## Sending it

Once approved, sending is the usual path:

```ts
await met.contacts.sendTemplate(contactId, {
  name: 'appointment_reminder',
  language: 'en',
  namespace: process.env.WA_TEMPLATE_NAMESPACE!,
  params: { '1': 'Ana', '2': 'Tuesday at 3' },
});
```

## Deleting it

```ts
await met.channels.deleteTemplate(whatsapp.id, { name: 'appointment_reminder' });
```

It stops existing for **all the numbers** on that account.

## And while we're here: quick replies

Quick replies — the saved answers your team drops into the chat — are in the API too,
and they have nothing to do with Meta: they're your own text, inside the workspace.

```ts
const saved = await met.conversations.quickReplies();
await met.conversations.createQuickReply({ title: 'Hours', content: 'We are open 8 to 6.' });
```

Reading requires `conversations:read`; creating, editing and deleting require
`conversations:write`.

## What's next

- [An agent that answers WhatsApp](recipe-whatsapp.html) — the 24-hour window and handing over to a person.
- [Events](events.html) — the log keeps 30 days, whether anyone is listening or not.
