● public · updated 22 days ago · id 93r6tg6w9x ·8,837 chars

Create & send a Meta WhatsApp template programmatically (Cloud API)

● Create & send a Meta WhatsApp template programmatically (Cloud API).md 8,837 chars · read-only
# Create & send a Meta WhatsApp template programmatically (Cloud API)

If you need to message a WhatsApp user **outside the 24-hour customer-service window** — first contact, marketing, transactional notifications — freeform text is rejected by Meta. You have to send an **approved Message Template**. This is the end-to-end flow: create the template in WhatsApp Manager, get it approved, send it via the Cloud API.

## 1. Prerequisites

- A WhatsApp Business Account (WABA) and a phone number connected to the **Cloud API hosted by Meta**.
- Admin access to that WABA in **Meta Business Manager**.
- A permanent **System User access token** with `whatsapp_business_messaging` + `whatsapp_business_management` scopes (don't use a temp dev token in production — it expires in 24h).
- The **Phone Number ID** (NOT the phone number) of the sender — find it in WhatsApp Manager → Phone numbers, or via `GET /{waba-id}/phone_numbers`.

## 2. Create the template in WhatsApp Manager

Go to **business.facebook.com → WhatsApp Manager → Message templates → Create template**.

⚠️ Don't confuse with **Template Groups** — that's just an analytics bucket for existing templates. The actual builder is under "Message templates".

Fill in:

| Field | Notes |
|---|---|
| **Category** | `Marketing` (promotions, announcements) · `Utility` (transactional: order updates, OTP-adjacent confirmations) · `Authentication` (OTP/2FA only). Wrong category is the #1 rejection reason. |
| **Name** | lowercase + underscores, e.g. `order_confirmation_v1`. You'll reference this exact string in the API call. |
| **Language** | Pick one (e.g. `English` = code `en`, `English (US)` = `en_US`). Add more languages later if you need to localize. |
| **Header** | Optional. Text, image, video, document, or location. |
| **Body** | The main text. Use positional placeholders `{{1}}`, `{{2}}` for dynamic fields. |
| **Footer** | Optional. Good place for `Reply STOP to unsubscribe` (often required to pass marketing review in stricter regions). |
| **Buttons** | Optional. Up to 10. Quick-reply buttons fire your webhook as `interactive` messages. URL/phone buttons take the user out of chat. |
| **Sample values** | Required. Provide example text for every `{{n}}` — Meta uses these to judge the template, not to send. |
| **Variable type** | Stick with **Number** (positional). "Named" looks nicer but is harder to call from code. |

### Body example
```
Your order #{{1}} ships on {{2}}. Track it here: {{3}}
```
Samples: `{{1}}=A1234`, `{{2}}=Tuesday`, `{{3}}=https://example.com/t/A1234`.

Click **Submit**. Status goes to **In review**. Approval is usually under an hour but can take up to 24h. If rejected, Meta gives a reason — fix and resubmit (keep the same name).

### Why templates get rejected
- Marketing template with no opt-out language.
- Promotional content tagged as Utility (or vice versa).
- URLs in samples that don't resolve.
- Placeholders that look like they'd render private data (PII).
- Emojis that violate policy (rare, but happens).

## 3. Test send via curl

Once Approved, send to your own phone first:

```bash
curl -sS -X POST "https://graph.facebook.com/v18.0/<PHONE_NUMBER_ID>/messages" \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "messaging_product": "whatsapp",
    "to": "<RECIPIENT_E164_NO_PLUS>",
    "type": "template",
    "template": {
      "name": "order_confirmation_v1",
      "language": { "code": "en" },
      "components": [
        {
          "type": "body",
          "parameters": [
            { "type": "text", "text": "A1234" },
            { "type": "text", "text": "Tuesday" },
            { "type": "text", "text": "https://example.com/t/A1234" }
          ]
        }
      ]
    }
  }'
```

Expected `200` response:
```json
{
  "messaging_product": "whatsapp",
  "contacts": [{"input": "919876543210", "wa_id": "919876543210"}],
  "messages": [{"id": "wamid.HBg...", "message_status": "accepted"}]
}
```

`accepted` ≠ delivered — it means Meta queued it. The WhatsApp message lands seconds later. Use the **message status webhook** to track `sent → delivered → read`.

### Pure-text template (no variables)
For something like the built-in `hello_world` template:
```json
{
  "messaging_product": "whatsapp",
  "to": "...",
  "type": "template",
  "template": { "name": "hello_world", "language": { "code": "en_US" } }
}
```
No `components` needed.

### Template with a header image
```json
"components": [
  { "type": "header", "parameters": [
      { "type": "image", "image": { "link": "https://example.com/hero.jpg" } }
  ]},
  { "type": "body", "parameters": [ ... ] }
]
```

### Template with a URL button (dynamic suffix)
If the button URL was defined as `https://example.com/order/{{1}}`:
```json
{ "type": "button", "sub_type": "url", "index": "0",
  "parameters": [ { "type": "text", "text": "A1234" } ] }
```

## 4. Programmatic send (Python)

```python
import httpx

API_URL = "https://graph.facebook.com/v18.0"

async def send_template(
    phone_number_id: str,
    access_token: str,
    to: str,
    template_name: str,
    language_code: str = "en",
    body_parameters: list[str] | None = None,
) -> dict:
    payload = {
        "messaging_product": "whatsapp",
        "recipient_type": "individual",
        "to": to,
        "type": "template",
        "template": {
            "name": template_name,
            "language": {"code": language_code},
        },
    }
    if body_parameters:
        payload["template"]["components"] = [{
            "type": "body",
            "parameters": [{"type": "text", "text": v} for v in body_parameters],
        }]

    async with httpx.AsyncClient(timeout=30.0) as client:
        r = await client.post(
            f"{API_URL}/{phone_number_id}/messages",
            headers={
                "Authorization": f"Bearer {access_token}",
                "Content-Type": "application/json",
            },
            json=payload,
        )
        r.raise_for_status()
        return r.json()
```

## 5. Common errors

| HTTP | Error | Fix |
|---|---|---|
| 400 | `(#132000) Number of parameters does not match the expected number` | You passed too few/many `{{n}}` values. Count them in the approved body. |
| 400 | `(#132001) Template name does not exist in the translation` | Wrong `language.code`. Must match what was approved (e.g. `en` vs `en_US`). |
| 400 | `(#132012) Parameter format does not match format in the created template` | Header/button params missing or in wrong shape. Check the components array. |
| 400 | `Template is paused or disabled` | Quality dropped (too many user blocks/reports). Pause the campaign, fix the content, resubmit. |
| 401/403 | `Invalid OAuth access token` | Token expired or scopes missing. Mint a permanent System User token. |
| 470 | `Re-engagement message` | The 24h window expired *and* you tried freeform instead of a template. Switch to a template. |

## 6. Things that bite in production

- **Phone-number-level template approval.** A template can be `Approved` at the WABA level but not enabled on every phone number in that WABA. Check WhatsApp Manager → Phone numbers → the number → Templates list.
- **Marketing throttling.** For users in some regions (India, notably), Meta limits how many marketing templates a single user can receive **across all businesses per day**. Some sends silently don't deliver. Track delivery webhooks, not just `accepted` responses.
- **Sender quality rating.** WhatsApp Manager shows `Green / Yellow / Red`. Drop to Red and your sending tier shrinks. Don't blast marketing to cold lists.
- **Opt-out handling.** When users reply STOP, you must stop sending them marketing templates within a reasonable window. Persist that state in your DB and check it before sending.
- **Idempotency.** Meta doesn't dedupe by your client-side message id — if you retry on a network error and the original actually delivered, the user gets two messages. Either confirm delivery via webhooks before retrying, or carry your own dedup key keyed on `(user_id, template_name, business_day)`.
- **Webhooks for inbound.** If your template has quick-reply buttons, users tapping them shows up in your webhook as `messages[].interactive.button_reply` — handle that shape, not just plain text.

## 7. Useful Meta API endpoints

- `POST /{phone-number-id}/messages` — send (template, text, image, etc.)
- `GET /{waba-id}/message_templates` — list templates, statuses, rejection reasons
- `POST /{waba-id}/message_templates` — create templates via API (alternative to the UI; same review pipeline)
- `DELETE /{waba-id}/message_templates?name={name}` — delete
- `GET /{phone-number-id}` — phone number metadata, quality rating, throughput limit

Docs: https://developers.facebook.com/docs/whatsapp/cloud-api/reference
feed this URL to your AI agent and watch it one-shot the task
https://npad.run/p/how-to-create-and-send-a-meta-whatsapp-template-via-cloud-ap-93r6tg6w9x
install npad

drop the URL into your AI agent — it pulls the note via npad's API and runs the whole thing in one shot.