# Send WhatsApp Template

> Send a Meta-approved WhatsApp template via POST /v1/whatsapp/templates — supports body params, TEXT-header variables, dynamic URL / QUICK_REPLY / OTP / COPY_CODE buttons, custom header media, and FLOW actions.

# Send a WhatsApp Template

Sends a pre-approved template to a WhatsApp recipient. Templates can be sent **at any time**, including outside Meta's 24-hour customer-service window — this is the right endpoint for proactive sends like order confirmations, appointment reminders, OTP verification, and re-engagement messages.

## Which endpoint do I call?

`POST` to `/v1/whatsapp/templates` over HTTPS. The endpoint requires the `whatsapp:send` scope on your API key — same scope as free-form text sends.

Not sure of a template's exact name, its language or what it needs? [List the number's templates](./list-templates) first: each one comes with its status, its placeholders and its buttons.

```
POST /v1/whatsapp/templates
```

## What does the request look like?

A minimal call needs an agent, a from-phone, a recipient, and the template name + language. Most real sends also include `parameters` to fill in placeholders, and many templates also require `buttons[]`, `header_parameters`, or `header_media_url` depending on what they were approved with.

```bash
curl -X POST https://api.mojeeb.app/v1/whatsapp/templates \
  -H "Authorization: Bearer mk_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "12345678-1234-1234-1234-123456789012",
    "from":     "+15557654321",
    "to":       "+15551234567",
    "template": {
      "name":     "arabic_signup_message",
      "language": "ar"
    }
  }'
```

With body parameters:

```bash
curl -X POST https://api.mojeeb.app/v1/whatsapp/templates \
  -H "Authorization: Bearer mk_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-12345-shipped" \
  -d '{
    "agent_id": "12345678-1234-1234-1234-123456789012",
    "from":     "+15557654321",
    "to":       "+15551234567",
    "template": {
      "name":     "order_shipped",
      "language": "en",
      "parameters": {
        "customer_name": "Sara",
        "tracking_url":  "https://example.com/track/A123"
      }
    }
  }'
```

## What goes in the request body?

The first five fields are required for any send. The rest are optional and only matter when your template was approved with the matching component.

| Field | Type | Required | Description |
|---|---|---|---|
| `agent_id` | UUID | Yes | The agent to send from. |
| `from` | E.164 string | Yes | The WhatsApp business number to send from. |
| `to` | E.164 string | Yes | Recipient's phone. |
| `template.name` | string | Yes | Meta-approved template name (e.g. `order_shipped`). |
| `template.language` | string | Yes | Meta's language code for the template, as the [template list](./list-templates) shows it (e.g. `en`, `ar`, `en_US`). |
| `template.parameters` | object | No | Body placeholders. Flat name → value map. Works for both POSITIONAL (`{{1}}`) and NAMED (`{{customer_name}}`) templates. |
| `template.header_parameters` | object | No | TEXT-header placeholder values. Independent from `parameters` — Meta numbers header and body placeholders separately. Header allows one placeholder maximum. |
| `template.header_media_url` | string | No | For IMAGE / VIDEO / DOCUMENT header templates — a public URL pointing at the asset to send. Omitted → Mojeeb uses the template's approval-time sample image. |
| `template.buttons` | array | No | Per-button values for templates with dynamic buttons. Each entry targets one button by its 0-based `index`. See [How do buttons work?](#how-do-buttons-work) |

## How do body parameters work?

Templates can have placeholders in their body — e.g. a template body of `Hello {{customer_name}}, your order is on the way: {{tracking_url}}`. Pass values as a flat map:

```json
"parameters": {
  "customer_name": "Sara",
  "tracking_url":  "https://example.com/track/A123"
}
```

For POSITIONAL templates (e.g. body `Hello {{1}}, your code is {{2}}`), use the placeholder number as the key:

```json
"parameters": {
  "1": "Sara",
  "2": "493281"
}
```

You don't need to tell us which style the template uses — Mojeeb detects it from the approved template body.

## How do TEXT-header variables work?

Templates with a TEXT header can have a single placeholder in the header text — for example a header of `Hello {{name}}`. Send the value separately from body parameters:

```bash
curl -X POST https://api.mojeeb.app/v1/whatsapp/templates \
  -H "Authorization: Bearer mk_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "12345678-1234-1234-1234-123456789012",
    "from":     "+15557654321",
    "to":       "+15551234567",
    "template": {
      "name":     "promo_with_header",
      "language": "en",
      "header_parameters": { "promo_label": "Summer Sale" },
      "parameters":        { "customer_name": "Sara", "discount_pct": "30" }
    }
  }'
```

Meta numbers header and body placeholders separately, so both can use `{{1}}` without conflict — keep them in their own field. Headers allow at most **one** placeholder; templates with multi-placeholder headers won't pass Meta approval.

## How do buttons work?

Templates can include dynamic buttons (URL, QUICK_REPLY, COPY_CODE, OTP, FLOW, MPM, CATALOG, ORDER_DETAILS). Each gets one entry in `template.buttons[]`, keyed by its 0-based `index` in the approved template. The [template list](./list-templates) gives each button's `index` and, as `send_field`, the field that carries its value. The fields you provide depend on the button's sub-type:

| Approved button | Field to send | Example value |
|---|---|---|
| **URL** (with `{{1}}` in URL) | `url_suffix` | `"orders/12345"` |
| **OTP** (AUTHENTICATION templates) | `otp_code` | `"493281"` |
| **QUICK_REPLY** | `quick_reply_payload` | `"FEEDBACK_GOOD"` |
| **COPY_CODE** (Marketing coupon) | `coupon_code` | `"SPRING30"` |
| **FLOW / MPM / SPM / CATALOG / ORDER_DETAILS** | `action` | object — see Meta's docs |
| **PHONE_NUMBER / VOICE_CALL** | — | Static; no value needed |

### Dynamic URL button

A template's URL button can be approved with a `{{1}}` at the end of the URL — Meta substitutes your value at send-time:

```json
"buttons": [
  { "index": 0, "url_suffix": "orders/12345" }
]
```

If the approved URL is `https://shop.example.com/{{1}}`, the customer sees a button that opens `https://shop.example.com/orders/12345`.

### QUICK_REPLY button

The `quick_reply_payload` is echoed back to your webhook when the user taps the button — it's how you distinguish which button was tapped. The button label itself is fixed at template approval; this is just the routing string:

```json
"buttons": [
  { "index": 0, "quick_reply_payload": "FEEDBACK_GOOD" },
  { "index": 1, "quick_reply_payload": "FEEDBACK_BAD" }
]
```

### COPY_CODE button (Marketing coupon)

Maximum 15 characters per Meta's rules. The customer sees a button that copies the code to their clipboard:

```json
"buttons": [
  { "index": 0, "coupon_code": "SPRING30" }
]
```

### OTP button (AUTHENTICATION templates)

For one-tap, copy-code, and zero-tap OTP templates, send the verification code via `otp_code`. The code typically also appears in the body `{{1}}`, so pass it in both places:

```bash
curl -X POST https://api.mojeeb.app/v1/whatsapp/templates \
  -H "Authorization: Bearer mk_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "12345678-1234-1234-1234-123456789012",
    "from":     "+15557654321",
    "to":       "+15551234567",
    "template": {
      "name":     "otp_verification_v1",
      "language": "en",
      "parameters": { "1": "493281" },
      "buttons": [
        { "index": 0, "otp_code": "493281" }
      ]
    }
  }'
```

Both copy-code and one-tap OTP buttons use the same wire shape — Meta picks the right rendering based on how the template was approved. `otp_code` is a friendlier alias for `url_suffix`; either works.

### FLOW button

For interactive Flow templates, supply the action payload Meta documents. Mojeeb forwards it verbatim:

```json
"buttons": [
  {
    "index": 0,
    "action": {
      "flow_token": "tok_3f4a91",
      "flow_action_data": { "branch_id": "cairo-downtown" }
    }
  }
]
```

See Meta's [Flow Templates docs](https://developers.facebook.com/docs/whatsapp/cloud-api/messages/interactive-flow-messages) for the full action shape per flow.

### Static buttons (PHONE_NUMBER, VOICE_CALL, static URL)

If the approved button has no placeholder (e.g. a fixed phone number, or a URL with no `{{1}}`), omit it from `buttons[]`. Meta uses the value baked into the template.

## How do custom header images work?

For templates with an IMAGE, VIDEO, or DOCUMENT header, you can send a different asset to each recipient via `header_media_url`:

```bash
curl -X POST https://api.mojeeb.app/v1/whatsapp/templates \
  -H "Authorization: Bearer mk_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_id": "12345678-1234-1234-1234-123456789012",
    "from":     "+15557654321",
    "to":       "+15551234567",
    "template": {
      "name":     "order_shipped_with_photo",
      "language": "en",
      "header_media_url": "https://cdn.customer.example.com/shipments/A12345.jpg",
      "parameters": { "tracking_no": "EG-A12345" }
    }
  }'
```

Mojeeb checks one thing before queueing: the URL must be an absolute `https://` URL, or the request fails with 422 `template_header_media_url_invalid`. Meta enforces the rest:

- **Must be publicly reachable from Meta's servers** — Meta downloads the asset itself, so intranet, VPN-only, or IP-allowlisted URLs won't work.
- **MIME and size must match Meta's per-format limits** — typically JPG/PNG ≤ 5 MB for IMAGE, MP4 ≤ 16 MB for VIDEO, PDF ≤ 100 MB for DOCUMENT.
- If the URL is unreachable or rejected, the send transitions to `status: failed` (visible via [Get Message Status](./message-status)).

When you omit `header_media_url`, Mojeeb sends the approval-time sample image for every recipient — useful when the same asset is fine for everyone.

## What does a success response look like?

`HTTP 202 Accepted` with the message object: its `id`, `status: "queued"`, the `agent_id` and `to` you sent, `type: "template"`, and `created_at`. Every field, with an example, is in the [reference for this endpoint](../api-reference/send-whats-app-template), rendered from the API's own contract so it is never out of date.

Use the returned `id` with [Get Message Status](./message-status) to track delivery.

## What can go wrong?

The standard [error envelope](../errors). Common template cases (the full catalog is on the [Errors](../errors) page):

| `code` | HTTP | When |
|---|---|---|
| `invalid_request_body` | 422 | A required field is missing or blank (for example `template.name` or `template.language`) — `param` names which |
| `from_phone_not_found_for_agent` | 422 | `from` doesn't match any active WhatsApp connection — `available_phones` lists what would work |
| `agent_not_authorized` | 403 | API key isn't allowed to use this agent |
| `template_not_found` | 422 | The connection has no template with that name, whatever its status — `available_templates` lists the names it has |
| `template_not_found_for_language` | 422 | The template doesn't exist in that language — `available_languages` lists the languages it has |
| `template_header_media_url_invalid` | 422 | `header_media_url` isn't an absolute HTTPS URL |

These errors fire **before** the send is queued (synchronous 422). Meta-side failures surface asynchronously, as do Mojeeb's send-time checks for missing button or header values — the request still returns 202 with a queued message id, and you discover the failure by polling [Get Message Status](./message-status): `status` becomes `failed` and `failure_reason` says why (`TemplatePaused`, `TemplateBroken`, `RecipientUnreachable` and others).

## Common questions

### How do I create a template?

Use Mojeeb's WhatsApp page: it creates, edits, deletes and submits templates on every plan that includes WhatsApp; see [WhatsApp Templates](/docs/channels/whatsapp-templates). Meta reviews each one, and once it's approved you can send it. The API doesn't create or edit templates yet: [List WhatsApp Templates](./list-templates) lists them, with each one's status.

### What's the difference between language `en` and `en_US`?

Both are Meta's template language codes. Meta uses them to pick the right template translation: `en` is generic English, and `en_US` is US English specifically. They aren't BCP-47 tags: Meta writes a regional variant with an underscore (`en_US`), not a hyphen (`en-US`). Send the exact code your template was approved with, as the [template list](./list-templates) shows it.

### Can I omit `parameters` if my template has no placeholders?

Yes. Omit the `parameters` field entirely (or pass an empty object). Templates with no placeholders ignore parameters anyway. Same applies to `header_parameters`, `header_media_url`, and `buttons`.

### Does the API validate parameters against the template's expected placeholders?

Partly. Mojeeb checks that the connection has a template with that name in your language, whatever its status, and that `header_media_url` is an absolute HTTPS URL. Those failures return a 422 with a specific `code` before the send is queued. A template that isn't approved passes this check and then fails at Meta, so [list the templates](./list-templates) and send only those with `status: approved`. When Mojeeb can't read the templates from Meta, or the number has more templates than Mojeeb reads and yours isn't among them, it skips the name and language check: the send is accepted (`202`), and Meta's verdict shows in the message's status.

Mojeeb also checks two things when the queued send runs. First, every dynamic button that needs a value has one: a URL button with a placeholder, OTP, COPY_CODE, FLOW and the other action buttons. QUICK_REPLY falls back to its label. Second, a TEXT header with placeholders gets at least one `header_parameters` value. If one is missing, the request still returns 202. The message then becomes `status: failed` with `failure_reason: TemplateBroken`, `failure_code: null` and a `failure_detail` that names the button index or header placeholders.

Mojeeb doesn't check placeholder values themselves, how many body or header values you send, or whether they match. Meta checks those, and a mismatch also shows up as `status: failed` when you look up the message.

### Can I send the same template twice safely?

Yes — use `Idempotency-Key`. See [Idempotency](../idempotency).

### Can I upload an image directly instead of hosting it myself?

Not in v1 — `header_media_url` requires you to host the asset on a public HTTPS URL. Customers with private CDNs need to expose a Meta-reachable URL (signed URLs, a public bucket, etc.). A direct-upload endpoint may land in a future version.

### What's the maximum size for `header_media_url`?

Limits are Meta's, not Mojeeb's. As of 2026: JPG/PNG ≤ 5 MB, MP4 ≤ 16 MB, PDF ≤ 100 MB. See [Meta's media specs](https://developers.facebook.com/docs/whatsapp/cloud-api/reference/media#supported-media-types) for the current numbers.

### How do I find a button's `index`?

[List the number's templates](./list-templates): each button comes with its `index`, and `send_field` names the field its value goes in. Buttons are 0-based in the order they appear in the approved template. The first button is `index: 0`, the second is `index: 1`, etc. Static buttons (PHONE_NUMBER, VOICE_CALL, URL without a placeholder) keep their index even though you don't send values for them — pass dynamic-button values keyed by their literal position in the template.
