> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lehar.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# WhatsApp campaigns

> Send an approved template, hold a conversation, and escalate to a voice call when the recipient wants one.

WhatsApp campaigns reach recipients over text and — for `whatsapp_voice` — escalate to a phone call when the conversation calls for it. This guide covers the message flow, the two timers that govern sends, templates, and inbound handling.

## The `whatsapp_voice` flow

Each recipient runs through send → wait → decide → act:

```mermaid theme={null}
flowchart TD
  T["Send approved template"] --> W{Wait for reply}
  W -->|reminder due| R[Send template reminder]
  R --> W
  W -->|reply received| D["Decide: opt-out check → optional KB → LLM"]
  W -->|timeout| FT[Finalize: no_response]
  D -->|call| V["Escalate to outbound voice call"]
  D -->|reply| A[Send free-form answer]
  D -->|opted_out| FO[Finalize: opted_out + suppress]
  D -->|no_call_required| FN[Finalize: no_call_required]
  A --> W
```

<Steps>
  <Step title="Send template">
    The campaign re-checks opt-out status and required template variables, then sends the **approved template**. The provider receives only the template variables; Lehar renders the approved body into the stored transcript for the dashboard and the decision model.
  </Step>

  <Step title="Wait">
    Waits for a reply, a terminal delivery failure, or the response timeout — sending up to `reminder_count` template reminders while it waits.
  </Step>

  <Step title="Decide">
    A deterministic opt-out keyword check runs first (no model). Then an optional [knowledge base](/concepts/knowledge-base) lookup, then a structured LLM decision → `call` | `reply` | `opted_out` | `no_call_required`. Only this campaign's messages feed the decision context, so past campaigns don't leak in.
  </Step>

  <Step title="Act">
    `call` hands off to the [outbound voice loop](/guides/campaign-lifecycle); `reply` sends an in-window free-form answer; `opted_out` / `no_call_required` / timeout finalize the recipient.
  </Step>
</Steps>

## Two independent timers

<Warning>
  Don't conflate these — they govern different things.
</Warning>

* **24-hour service window** — whether a **free-form** message may be sent at all. It's opened by the recipient's last inbound message and is re-validated against the clock at send time. Outside the window, only approved **templates** may be sent.
* **`response_timeout_seconds`** — the orchestration timer for how long to wait before a reminder or giving up. Independent of the window.

Reminders while waiting are **templates** (the window may not be open yet). The post-decision `reply` is **free-form** (the window is open because the recipient just replied).

## Templates

WhatsApp sends use pre-approved templates from a **customer-scoped registry**. Administrators add approved templates per workspace; customers get a read-only view that feeds the dashboard dropdown and CSV columns.

| Method | Path                       | Auth                                            |
| ------ | -------------------------- | ----------------------------------------------- |
| GET    | `/whatsapp/templates`      | `campaigns:read`                                |
| POST   | `/whatsapp/templates`      | `campaigns:write` + admin (customers read-only) |
| DELETE | `/whatsapp/templates/{id}` | `campaigns:write` + admin (customers read-only) |

A template carries `template_id`, optional `name`, `language`, `category`, `variables[]`, and an optional `body_template` (the approved body, rendered into internal transcripts). The campaign's `workflow.whatsapp` config maps each template placeholder to a recipient variable or a literal fallback.

## Configuration shape

`whatsapp_voice` config is stored on the campaign under `workflow.whatsapp`:

```json theme={null}
{
  "whatsapp": {
    "template": { "id": "renewal_v1", "language": "en", "variables": { "1": "name" }, "required_vars": ["name"] },
    "reminder_template": { "id": "renewal_reminder_v1", "language": "en" },
    "response_timeout_seconds": 86400,
    "reminder_count": 1,
    "prompt": { "objective": "", "instructions": "", "task": "", "faqs": [], "sample_conversations": [] },
    "kb": { "enabled": false, "agent_id": null, "top_k": 3, "threshold": 50 }
  }
}
```

## Inbound messages

Replies and delivery receipts arrive at `POST /webhooks/whatsapp/inbound` (provider-authenticated). Handling is **idempotent** on the provider's message id and applies delivery statuses **monotonically** (a `read` is never regressed to `delivered`). An inbound reply opens the service window and wakes the waiting recipient. See [Webhooks & Events](/guides/webhooks#whatsapp-inbound).

## Finalization states

A recipient finalizes as one of: `completed`, `no_response`, `opted_out`, `no_call_required`, or `failed`. An `opted_out` result also writes a workspace-level suppression so future campaigns honor it. The `call` branch finalizes through the reused voice path.

## Reading conversations

| Method | Path                            | Scope            |
| ------ | ------------------------------- | ---------------- |
| GET    | `/campaigns/{id}/conversations` | `campaigns:read` |
| GET    | `/campaigns/{id}/messages`      | `campaigns:read` |
| GET    | `/whatsapp/sessions`            | `campaigns:read` |

<Note>
  `GET /campaigns/{id}/events` includes the structured `campaign.whatsapp_decision` entries (decision, reason, and reply text), which is the clearest way to audit why a recipient was called, answered, or suppressed.
</Note>
