# Templates

> Pre-approved messages for WhatsApp Business outside the 24-hour window

Templates are pre-approved message formats required for contacting users outside the 24-hour conversation window. They must be submitted to Meta for approval before use.

## Why Templates?

- **Required by WhatsApp** - Only way to message users outside 24-hour window
- **Consistency** - Ensure uniform messaging across your team
- **Compliance** - Meta reviews content for quality and policy compliance
- **Analytics** - Track performance by template

## Quick Start: Send a Template Message

If you already have an approved template, here's how to send it with variables:

```typescript
const message = await zavu.messages.send({
  to: "+56912345678",
  messageType: "template",
  content: {
    templateId: "tpl_abc123",
    templateVariables: {
      "1": "John",
      "2": "ORD-12345",
      "3": "January 20, 2025"
    }
  }
});
```

```bash
curl -X POST https://api.zavu.dev/v1/messages \
  -H "Authorization: Bearer zv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+56912345678",
    "messageType": "template",
    "content": {
      "templateId": "tpl_abc123",
      "templateVariables": {
        "1": "John",
        "2": "ORD-12345",
        "3": "January 20, 2025"
      }
    }
  }'
```

### How Variables Work

A template can use one of two placeholder formats, and you pass values the same way for both:

- **Positional**: `{{1}}`, `{{2}}`, `{{3}}` — keyed by position.
- **Named**: `{{customer_name}}`, `{{order_id}}` — keyed by name.

```
Template body:  "Hi {{1}}, your order #{{2}} is confirmed. Delivery: {{3}}."
                     ^              ^                              ^
                     |              |                              |
Variables:      "1": "John"   "2": "ORD-12345"          "3": "January 20, 2025"
```

In both cases you provide the values through `templateVariables`. The keys match
the placeholders in the template body — numbers for positional templates, names
for named templates:

```json
// Positional template
"templateVariables": { "1": "John", "2": "ORD-12345" }

// Named template
"templateVariables": { "customer_name": "John", "order_id": "ORD-12345" }
```

> **Warning:**
> You must provide values for **all** variables defined in the template. Missing variables will cause the message to fail (`#100 INVALID_PARAMETER` from Meta).

> **Warning:**
> A variable value may not contain **line breaks, tabs or more than 4 consecutive spaces**. WhatsApp refuses those parameters, so Zavu rejects the request with `400 invalid_request` before sending, and `details.field` names the value (for example `content.templateVariables.2`). Flatten multi-line text first, for example by replacing line breaks with ` · `. Templates sent over SMS or Telegram accept line breaks.

#### Named vs Positional — which does my template use?

You normally don't need to care: Zavu detects the format per template and builds the correct Meta payload. The distinction matters only when you decide what keys to use in `templateVariables`:

| How the template was created | Format at Meta | Keys to use |
|---|---|---|
| Created in Zavu (`POST /v1/templates`) | Positional — Zavu converts `{{name}}` → `{{1}}` at submit time | Position numbers (`"1"`, `"2"`) **or** the original names |
| Imported from an existing WhatsApp Business Account | Whatever Meta has on file — **named or positional** | Match the placeholders in the template body |

> **Tip:**
> Fetch the template (`GET /v1/templates/{id}`) and read its `body` to see which placeholders it expects. If the body shows `{{customer_name}}`, use `{ "customer_name": "..." }`; if it shows `{{1}}`, use `{ "1": "..." }`.

### Text Header Variables

A template can have a **text header** with its own variable, e.g. `Invitación Boda {{novios}}`. The header variable is independent from the body variables, but you pass its value through the **same** `templateVariables` object, keyed by the header placeholder's name (or `{{1}}` for a positional header):

```typescript
// Template header: "Invitación Boda {{novios}}"
// Template body:   "Estimado/a {{nombre}}, te corresponden {{pases}} boletos."
await zavu.messages.send({
  to: "+5212345678901",
  messageType: "template",
  content: {
    templateId: "tpl_wedding",
    templateVariables: {
      novios: "Jorge y Laura",   // header variable
      nombre: "Nestor Alvares",  // body variables
      pases: "4"
    }
  }
});
```

```bash
curl -X POST https://api.zavu.dev/v1/messages \
  -H "Authorization: Bearer zv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+5212345678901",
    "messageType": "template",
    "content": {
      "templateId": "tpl_wedding",
      "templateVariables": {
        "novios": "Jorge y Laura",
        "nombre": "Nestor Alvares",
        "pases": "4"
      }
    }
  }'
```

> **Note:**
> WhatsApp text headers allow **at most one** variable. Static text headers (no placeholder) need no value — Meta has the text baked into the approved template.

## Media headers

When a template's header is an image, video or document, Zavu sends the file stored on the template. To send a different file in one message (for example a per-customer PDF), pass `templateHeaderMediaUrl`. For a document header you can also set the file name the recipient sees with `templateHeaderFilename`.

```typescript
await zavu.messages.send({
  to: "+56912345678",
  messageType: "template",
  content: {
    templateId: "tmpl_abc123", // header type: document
    templateVariables: { "1": "Maria", "2": "INV-1042" },
    templateHeaderMediaUrl: "https://files.example.com/invoices/INV-1042.pdf?sig=abc123",
    templateHeaderFilename: "invoice-1042.pdf",
  },
});
```

```bash
curl -X POST https://api.zavu.dev/v1/messages \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+56912345678",
    "messageType": "template",
    "content": {
      "templateId": "tmpl_abc123",
      "templateVariables": { "1": "Maria", "2": "INV-1042" },
      "templateHeaderMediaUrl": "https://files.example.com/invoices/INV-1042.pdf?sig=abc123",
      "templateHeaderFilename": "invoice-1042.pdf"
    }
  }'
```

- `templateHeaderMediaUrl` must be an HTTPS URL that WhatsApp can fetch when the message is sent. It replaces the template's stored file for this message only.
- `templateHeaderFilename` is only valid when the template's header is `document`. On its own it names the template's stored file.
- Both fields are refused with `400 invalid_request` (`details.field` names the field) when the template's header does not match, before anything is sent or charged.
- Without these fields, the file stored on the template is sent, as before.

## Full Workflow

Templates follow a 3-step process: **Create** -> **Submit for Approval** -> **Send**.

### Step 1: Create a Template

```typescript
const template = await zavu.templates.create({
  name: "order_confirmation",
  language: "en",
  body: "Hi {{1}}, your order #{{2}} has been confirmed! Estimated delivery: {{3}}.",
  whatsappCategory: "UTILITY",
  variables: ["customer_name", "order_id", "delivery_date"]
});

console.log(template.id);     // "tpl_abc123"
console.log(template.status); // "draft"
```

```bash
curl -X POST https://api.zavu.dev/v1/templates \
  -H "Authorization: Bearer zv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "order_confirmation",
    "language": "en",
    "body": "Hi {{1}}, your order #{{2}} has been confirmed! Estimated delivery: {{3}}.",
    "whatsappCategory": "UTILITY",
    "variables": ["customer_name", "order_id", "delivery_date"]
  }'
```

The `variables` array is optional and purely for documentation - it helps you remember what each position represents.

### Step 2: Submit for Approval

Templates start in `draft` status. You need to explicitly submit them to Meta for review:

```typescript
const submitted = await zavu.templates.submit("tpl_abc123", {
  senderId: "sender_12345",
  category: "UTILITY"
});

console.log(submitted.status); // "pending"
```

```bash
curl -X POST https://api.zavu.dev/v1/templates/tpl_abc123/submit \
  -H "Authorization: Bearer zv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "senderId": "sender_12345",
    "category": "UTILITY"
  }'
```

> **Note:**
> The `senderId` must be a sender with a WhatsApp Business Account connected. The template is submitted to Meta through that WABA.

### Step 3: Check Approval Status

```bash
curl https://api.zavu.dev/v1/templates/tpl_abc123 \
  -H "Authorization: Bearer zv_live_xxx"
```

```json
{
  "id": "tpl_abc123",
  "name": "order_confirmation",
  "status": "approved",
  "category": "UTILITY",
  "body": "Hi {{1}}, your order #{{2}} has been confirmed! Estimated delivery: {{3}}.",
  "variables": ["customer_name", "order_id", "delivery_date"]
}
```

| Status | Description |
|--------|-------------|
| `draft` | Created but not yet submitted to Meta |
| `pending` | Submitted, awaiting Meta review |
| `approved` | Ready to use |
| `rejected` | Not approved - check rejection reason |

> **Warning:**
> Only `approved` templates can be used to send messages. Attempting to use draft, pending, or rejected templates will fail.

### Step 4: Send the Template

Once approved, send it using the positional variables (see [Quick Start](#quick-start-send-a-template-message) above).

## Template Categories

| Category | Use Case | Approval Speed |
|----------|----------|----------------|
| `UTILITY` | Order updates, shipping, account alerts | Fast (hours) |
| `MARKETING` | Promotions, offers, newsletters | Slower (days) |
| `AUTHENTICATION` | OTPs, verification codes | Fast (hours) |

> **Note:**
> Choose the category that best matches your use case. Miscategorization can lead to rejection.

## Templates with Buttons

Add call-to-action or quick reply buttons:

```bash
curl -X POST https://api.zavu.dev/v1/templates \
  -H "Authorization: Bearer zv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "order_delivered",
    "language": "en",
    "body": "Hi {{1}}, your order has been delivered!",
    "whatsappCategory": "UTILITY",
    "variables": ["customer_name"],
    "buttons": [
      {
        "type": "url",
        "text": "Track Order",
        "url": "https://example.com/track/{{1}}"
      },
      {
        "type": "quick_reply",
        "text": "Rate Delivery"
      }
    ]
  }'
```

### Button Types

| Type | Description | Max |
|------|-------------|-----|
| `quick_reply` | User response button | 3 buttons |
| `url` | Opens a URL (can include variables) | - |
| `phone` | Initiates a phone call | - |
| `otp` | Copy code / one-tap autofill for authentication | - |

### Sending Templates with Dynamic URL Buttons

When a template has a URL button with a `{{1}}` placeholder, pass the substitution under `templateButtonVariables` keyed by the **button's position** in the template's `buttons` array.

`templateVariables` is for body placeholders. `templateButtonVariables` is for buttons. They use **different keys**:

- `templateVariables` keys → position of the placeholder in the body text (`"1"`, `"2"`, ...).
- `templateButtonVariables` keys → index of the button in the `buttons` array (`"0"`, `"1"`, `"2"`).

> **Warning:**
> **WhatsApp URL buttons only accept `{{1}}` (positional, numeric, no whitespace, no name).** Even though body placeholders may use named parameters like `{{name}}`, URL buttons do not. Anything else (`{{token}}`, `{{ 1 }}`, `{{user.id}}`, etc.) is approved by Meta as literal text in the URL — there is no way to substitute it later.
>
> If your template was approved with a non-`{{1}}` placeholder in a URL button, you must recreate the template with `{{1}}` and resubmit it for approval. Zavu now returns `400 invalid_request` instead of silently delivering broken URLs like `https://...%7B%7Bvariable%7D%7D`.

Example. Suppose your approved template looks like this:

```json
{
  "id": "tpl_discipline_report",
  "body": "Hi {{1}}, your child received a {{2}}.",
  "buttons": [
    {
      "type": "url",
      "text": "View report",
      "url": "https://reporte.link/{{1}}"
    }
  ]
}
```

Send it like this:

```typescript
await zavu.messages.send({
  to: "+56912345678",
  messageType: "template",
  content: {
    templateId: "tpl_discipline_report",
    templateVariables: {
      "1": "Marco Ordaz",
      "2": "report for inappropriate behavior"
    },
    templateButtonVariables: {
      "0": "abc-report-token"
    }
  }
});
```

```bash
curl -X POST https://api.zavu.dev/v1/messages \
  -H "Authorization: Bearer zv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+56912345678",
    "messageType": "template",
    "content": {
      "templateId": "tpl_discipline_report",
      "templateVariables": {
        "1": "Marco Ordaz",
        "2": "report for inappropriate behavior"
      },
      "templateButtonVariables": {
        "0": "abc-report-token"
      }
    }
  }'
```

The recipient receives a button that opens `https://reporte.link/abc-report-token`.

#### Multiple buttons

For templates with several buttons, supply one entry per dynamic button. Static URL buttons (no `{{1}}`) and `quick_reply` buttons are not included in `templateButtonVariables`.

```json
{
  "buttons": [
    { "type": "url", "text": "View order", "url": "https://shop.example.com/orders/{{1}}" },
    { "type": "quick_reply", "text": "Track" },
    { "type": "url", "text": "Help", "url": "https://help.example.com/" }
  ]
}
```

```json
{
  "templateButtonVariables": {
    "0": "ORD-12345"
  }
}
```

Index `1` (quick reply) and index `2` (static URL) are skipped.

#### Validation rules

| Rule | Behavior |
|---|---|
| URL has `{{1}}` and `templateButtonVariables[index]` provided | ✅ delivered with substitution |
| URL has `{{1}}` and `templateButtonVariables[index]` missing | ❌ `400 invalid_request: Missing URL button parameter at index N` |
| URL has `{{name}}`, `{{ 1 }}`, `{{token}}`, etc. | ❌ `400 invalid_request: ... WhatsApp treats it as literal text. Recreate the template with "{{1}}" and resubmit it for approval.` |
| URL has more than one placeholder (e.g. `/{{1}}/{{2}}`) | ❌ `400 invalid_request: at most one positional variable` |
| URL has no placeholder (static link) | ✅ ignored — do not include the index in `templateButtonVariables` |

## Authentication Templates (OTP)

For verification codes, use `AUTHENTICATION` category with OTP buttons:

```bash
curl -X POST https://api.zavu.dev/v1/templates \
  -H "Authorization: Bearer zv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "login_otp",
    "language": "en",
    "body": "{{1}} is your verification code.",
    "whatsappCategory": "AUTHENTICATION",
    "variables": ["otp_code"],
    "buttons": [
      {
        "type": "otp",
        "text": "Copy Code",
        "otpType": "COPY_CODE"
      }
    ],
    "addSecurityRecommendation": true,
    "codeExpirationMinutes": 5
  }'
```

Send the OTP:

```bash
curl -X POST https://api.zavu.dev/v1/messages \
  -H "Authorization: Bearer zv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+56912345678",
    "messageType": "template",
    "content": {
      "templateId": "tpl_otp123",
      "templateVariables": {
        "1": "482910"
      }
    }
  }'
```

See the [OTP Templates guide](https://www.zavu.dev/docs/guides/whatsapp/templates/otp) for more details.

## Managing Templates

### List Templates

```bash
curl https://api.zavu.dev/v1/templates \
  -H "Authorization: Bearer zv_live_xxx"
```

### Delete Template

```bash
curl -X DELETE https://api.zavu.dev/v1/templates/tpl_abc123 \
  -H "Authorization: Bearer zv_live_xxx"
```

## Best Practices

### Naming Conventions

Use descriptive, lowercase names with underscores:

```
order_confirmation
shipping_update
password_reset
appointment_reminder
promotional_offer
```

### Content Guidelines

**Do:**
- Use clear, concise language
- Include dynamic variables for personalization
- Provide value to the recipient
- Test with real data before production use

**Don't:**
- Use placeholder text like "[insert name]"
- Include excessive capitalization or punctuation
- Send promotional content without consent
- Use misleading or clickbait content

## Common Rejection Reasons

| Reason | Solution |
|--------|----------|
| Variable syntax error | Use `{{1}}`, `{{2}}` positional format |
| Category mismatch | Choose correct category for content |
| Promotional in UTILITY | Use `MARKETING` category for promotions |
| Missing opt-out | Include unsubscribe option for marketing |
| Policy violation | Review Meta's commerce and messaging policies |

## Testing Templates

Test with your own phone number before sending to customers:

```bash
curl -X POST https://api.zavu.dev/v1/messages \
  -H "Authorization: Bearer zv_test_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+your_phone_number",
    "messageType": "template",
    "content": {
      "templateId": "tpl_abc123",
      "templateVariables": {
        "1": "Test User",
        "2": "TEST-001",
        "3": "Tomorrow"
      }
    }
  }'
```

> **Tip:**
> Use test mode API keys (`zv_test_xxx`) during development to avoid charges.
