Back to skills
SkillHub ClubAnalyze Data & AIFull StackData / AI

integrate-whatsapp

Connect WhatsApp to your product with Kapso: onboard customers with setup links, detect connections, receive events via webhooks, and send messages/templates/media. Also manage WhatsApp Flows (create/update/publish, data endpoints, encryption). Use when integrating WhatsApp end-to-end.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
53
Hot score
91
Updated
March 20, 2026
Overall rating
C3.1
Composite score
3.1
Best-practice grade
B75.6

Install command

npx @skill-hub/cli install gokapso-agent-skills-integrate-whatsapp

Repository

gokapso/agent-skills

Skill path: skills/integrate-whatsapp

Connect WhatsApp to your product with Kapso: onboard customers with setup links, detect connections, receive events via webhooks, and send messages/templates/media. Also manage WhatsApp Flows (create/update/publish, data endpoints, encryption). Use when integrating WhatsApp end-to-end.

Open repository

Best for

Primary workflow: Analyze Data & AI.

Technical facets: Full Stack, Data / AI.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: gokapso.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install integrate-whatsapp into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/gokapso/agent-skills before adding integrate-whatsapp to shared team environments
  • Use integrate-whatsapp for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: integrate-whatsapp
description: "Connect WhatsApp to your product with Kapso: onboard customers with setup links, detect connections, receive events via webhooks, and send messages/templates/media. Also manage WhatsApp Flows (create/update/publish, data endpoints, encryption). Use when integrating WhatsApp end-to-end."
---

# Integrate WhatsApp

## Setup

Env vars:
- `KAPSO_API_BASE_URL` (host only, no `/platform/v1`)
- `KAPSO_API_KEY`
- `META_GRAPH_VERSION` (optional, default `v24.0`)

Auth header (direct API calls):
```
X-API-Key: <api_key>
```

Install deps (once):
```bash
npm i
```

## Connect WhatsApp (setup links)

Typical onboarding flow:

1. Create customer: `POST /platform/v1/customers`
2. Generate setup link: `POST /platform/v1/customers/:id/setup_links`
3. Customer completes embedded signup
4. Use `phone_number_id` to send messages and configure webhooks

Detect connection:
- Project webhook `whatsapp.phone_number.created` (recommended)
- Success redirect URL query params (use for frontend UX)

Provision phone numbers (setup link config):
```json
{
  "setup_link": {
    "provision_phone_number": true,
    "phone_number_country_isos": ["US"]
  }
}
```

Notes:
- Platform API base: `/platform/v1`
- Meta proxy base: `/meta/whatsapp/v24.0` (messaging, templates, media)
- Use `phone_number_id` as the primary WhatsApp identifier

## Receive events (webhooks)

Use webhooks to receive:
- Project events (connection lifecycle, workflow events)
- Phone-number events (messages, conversations, delivery status)

Scope rules:
- **Project webhooks**: only project-level events (connection lifecycle, workflow events)
- **Phone-number webhooks**: only WhatsApp message + conversation events for that `phone_number_id`
- WhatsApp message/conversation events (`whatsapp.message.*`, `whatsapp.conversation.*`) are **phone-number only**

Create a webhook:
- Project-level: `node scripts/create.js --scope project --url <https://...> --events <csv>`
- Phone-number: `node scripts/create.js --phone-number-id <id> --url <https://...> --events <csv>`

Common flags for create/update:
- `--url <https://...>` - webhook destination
- `--events <csv|json-array>` - event types (Kapso webhooks)
- `--kind <kapso|meta>` - Kapso (event-based) vs raw Meta forwarding
- `--payload-version <v1|v2>` - payload format (`v2` recommended)
- `--buffer-enabled <true|false>` - enable buffering for `whatsapp.message.received`
- `--buffer-window-seconds <n>` - 1-60 seconds
- `--max-buffer-size <n>` - 1-100
- `--active <true|false>` - enable/disable

Test delivery:
```bash
node scripts/test.js --webhook-id <id>
```

Always verify signatures. See:
- `references/webhooks-overview.md`
- `references/webhooks-reference.md`

## Send and read messages

### Discover IDs first

Two Meta IDs are needed for different operations:

| ID | Used for | How to discover |
|----|----------|-----------------|
| `business_account_id` (WABA) | Template CRUD | `node scripts/list-platform-phone-numbers.mjs` |
| `phone_number_id` | Sending messages, media upload | `node scripts/list-platform-phone-numbers.mjs` |

### SDK setup

Install:
```bash
npm install @kapso/whatsapp-cloud-api
```

Create client:
```ts
import { WhatsAppClient } from "@kapso/whatsapp-cloud-api";

const client = new WhatsAppClient({
  baseUrl: "https://api.kapso.ai/meta/whatsapp",
  kapsoApiKey: process.env.KAPSO_API_KEY!
});
```

### Send a text message

Via SDK:
```ts
await client.messages.sendText({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  to: "+15551234567",
  body: "Hello from Kapso"
});
```

### Send a template message

1. Discover IDs: `node scripts/list-platform-phone-numbers.mjs`
2. Draft template payload from `assets/template-utility-order-status-update.json`
3. Create: `node scripts/create-template.mjs --business-account-id <WABA_ID> --file <payload.json>`
4. Check status: `node scripts/template-status.mjs --business-account-id <WABA_ID> --name <name>`
5. Send: `node scripts/send-template.mjs --phone-number-id <ID> --file <send-payload.json>`

### Send an interactive message

Interactive messages require an active 24-hour session window. For outbound notifications outside the window, use templates.

1. Discover `phone_number_id`
2. Pick payload from `assets/send-interactive-*.json`
3. Send: `node scripts/send-interactive.mjs --phone-number-id <ID> --file <payload.json>`

### Read inbox data

Use Meta proxy or SDK:
- Proxy: `GET /{phone_number_id}/messages`, `GET /{phone_number_id}/conversations`
- SDK: `client.messages.query()`, `client.conversations.list()`

### Template rules

Creation:
- Use `parameter_format: "NAMED"` with `{{param_name}}` (preferred over positional)
- Include examples when using variables in HEADER/BODY
- Use `language` (not `language_code`)
- Don't interleave QUICK_REPLY with URL/PHONE_NUMBER buttons
- URL button variables must be at the end of the URL and use positional `{{1}}`

Send-time:
- For NAMED templates, include `parameter_name` in header/body params
- URL buttons need a `button` component with `sub_type: "url"` and `index`
- Media headers use either `id` or `link` (never both)

## WhatsApp Flows

Use Flows to build native WhatsApp forms. Read `references/whatsapp-flows-spec.md` before editing Flow JSON.

### Create and publish a flow

1. Create flow: `node scripts/create-flow.js --phone-number-id <id> --name <name>`
2. Update JSON: `node scripts/update-flow-json.js --flow-id <id> --json-file <path>`
3. Publish: `node scripts/publish-flow.js --flow-id <id>`
4. Test: `node scripts/send-test-flow.js --phone-number-id <id> --flow-id <id> --to <phone>`

### Attach a data endpoint (dynamic flows)

1. Set up encryption: `node scripts/setup-encryption.js --flow-id <id>`
2. Create endpoint: `node scripts/set-data-endpoint.js --flow-id <id> --code-file <path>`
3. Deploy: `node scripts/deploy-data-endpoint.js --flow-id <id>`
4. Register: `node scripts/register-data-endpoint.js --flow-id <id>`

### Flow JSON rules

Static flows (no data endpoint):
- Use `version: "7.3"`
- `routing_model` and `data_api_version` are optional
- See `assets/sample-flow.json`

Dynamic flows (with data endpoint):
- Use `version: "7.3"` with `data_api_version: "3.0"`
- `routing_model` is required (defines valid screen transitions)
- See `assets/dynamic-flow.json`

### Data endpoint rules

Handler signature:
```js
async function handler(request, env) {
  const body = await request.json();
  // body.data_exchange.action: INIT | data_exchange | BACK
  // body.data_exchange.screen: current screen id
  // body.data_exchange.data: user inputs
  return Response.json({
    version: "3.0",
    screen: "NEXT_SCREEN_ID",
    data: { }
  });
}
```

- Do not use `export` or `module.exports`
- Completion uses `screen: "SUCCESS"` with `extension_message_response.params`
- Do not include `endpoint_uri` or `data_channel_uri` (Kapso injects these)

### Troubleshooting

- Preview shows `"flow_token is missing"`: flow is dynamic without a data endpoint. Attach one and refresh.
- Encryption setup errors: enable encryption in Settings for the phone number/WABA.
- OAuthException 139000 (Integrity): WABA must be verified in Meta security center.

## Scripts

### Webhooks

| Script | Purpose |
|--------|---------|
| `list.js` | List webhooks |
| `get.js` | Get webhook details |
| `create.js` | Create a webhook |
| `update.js` | Update a webhook |
| `delete.js` | Delete a webhook |
| `test.js` | Send a test event |

### Messaging and templates

| Script | Purpose | Required ID |
|--------|---------|-------------|
| `list-platform-phone-numbers.mjs` | Discover business_account_id + phone_number_id | — |
| `list-connected-numbers.mjs` | List WABA phone numbers | business_account_id |
| `list-templates.mjs` | List templates (with filters) | business_account_id |
| `template-status.mjs` | Check single template status | business_account_id |
| `create-template.mjs` | Create a template | business_account_id |
| `update-template.mjs` | Update existing template | business_account_id |
| `send-template.mjs` | Send template message | phone_number_id |
| `send-interactive.mjs` | Send interactive message | phone_number_id |
| `upload-media.mjs` | Upload media for send-time headers | phone_number_id |

### Flows

| Script | Purpose |
|--------|---------|
| `list-flows.js` | List all flows |
| `create-flow.js` | Create a new flow |
| `get-flow.js` | Get flow details |
| `read-flow-json.js` | Read flow JSON |
| `update-flow-json.js` | Update flow JSON (creates new version) |
| `publish-flow.js` | Publish a flow |
| `get-data-endpoint.js` | Get data endpoint config |
| `set-data-endpoint.js` | Create/update data endpoint code |
| `deploy-data-endpoint.js` | Deploy data endpoint |
| `register-data-endpoint.js` | Register data endpoint with Meta |
| `get-encryption-status.js` | Check encryption status |
| `setup-encryption.js` | Set up flow encryption |
| `send-test-flow.js` | Send a test flow message |
| `delete-flow.js` | Delete a flow |
| `list-flow-responses.js` | List stored flow responses |
| `list-function-logs.js` | List function logs |
| `list-function-invocations.js` | List function invocations |

### OpenAPI

| Script | Purpose |
|--------|---------|
| `openapi-explore.mjs` | Explore OpenAPI (search/op/schema/where) |

Examples:
```bash
node scripts/openapi-explore.mjs --spec whatsapp search "template"
node scripts/openapi-explore.mjs --spec whatsapp op sendMessage
node scripts/openapi-explore.mjs --spec whatsapp schema TemplateMessage
node scripts/openapi-explore.mjs --spec platform ops --tag "WhatsApp Flows"
node scripts/openapi-explore.mjs --spec platform op setupWhatsappFlowEncryption
node scripts/openapi-explore.mjs --spec platform search "setup link"
```

## Assets

| File | Description |
|------|-------------|
| `template-utility-order-status-update.json` | UTILITY template with named params + URL button |
| `send-template-order-status-update.json` | Send-time payload for order_status_update |
| `template-utility-named.json` | UTILITY template showing button ordering rules |
| `template-marketing-media-header.json` | MARKETING template with IMAGE header |
| `template-authentication-otp.json` | AUTHENTICATION OTP template (COPY_CODE) |
| `send-interactive-buttons.json` | Interactive button message |
| `send-interactive-list.json` | Interactive list message |
| `send-interactive-cta-url.json` | Interactive CTA URL message |
| `send-interactive-location-request.json` | Location request message |
| `send-interactive-catalog-message.json` | Catalog message |
| `sample-flow.json` | Static flow example (no endpoint) |
| `dynamic-flow.json` | Dynamic flow example (with endpoint) |
| `webhooks-example.json` | Webhook create/update payload example |

## References

- [references/getting-started.md](references/getting-started.md) - Platform onboarding
- [references/platform-api-reference.md](references/platform-api-reference.md) - Full endpoint reference
- [references/setup-links.md](references/setup-links.md) - Setup link configuration
- [references/detecting-whatsapp-connection.md](references/detecting-whatsapp-connection.md) - Connection detection methods
- [references/webhooks-overview.md](references/webhooks-overview.md) - Webhook types, signature verification, retries
- [references/webhooks-event-types.md](references/webhooks-event-types.md) - Available events
- [references/webhooks-reference.md](references/webhooks-reference.md) - Webhook API and payload notes
- [references/templates-reference.md](references/templates-reference.md) - Template creation rules, components cheat sheet, send-time components
- [references/whatsapp-api-reference.md](references/whatsapp-api-reference.md) - Meta proxy payloads for messages and conversations
- [references/whatsapp-cloud-api-js.md](references/whatsapp-cloud-api-js.md) - SDK usage for sending and reading messages
- [references/whatsapp-flows-spec.md](references/whatsapp-flows-spec.md) - Flow JSON spec

## Related skills

- `automate-whatsapp` - Workflows, agents, and automations
- `observe-whatsapp` - Debugging, logs, health checks

<!-- FILEMAP:BEGIN -->
```text
[integrate-whatsapp file map]|root: .
|.:{package.json,SKILL.md}
|assets:{dynamic-flow.json,sample-flow.json,send-interactive-buttons.json,send-interactive-catalog-message.json,send-interactive-cta-url.json,send-interactive-list.json,send-interactive-location-request.json,send-template-order-status-update.json,template-authentication-otp.json,template-marketing-media-header.json,template-utility-named.json,template-utility-order-status-update.json,webhooks-example.json}
|references:{detecting-whatsapp-connection.md,getting-started.md,platform-api-reference.md,setup-links.md,templates-reference.md,webhooks-event-types.md,webhooks-overview.md,webhooks-reference.md,whatsapp-api-reference.md,whatsapp-cloud-api-js.md,whatsapp-flows-spec.md}
|scripts:{create-flow.js,create-function.js,create-template.mjs,create.js,delete-flow.js,delete.js,deploy-data-endpoint.js,deploy-function.js,get-data-endpoint.js,get-encryption-status.js,get-flow.js,get-function.js,get.js,list-connected-numbers.mjs,list-flow-responses.js,list-flows.js,list-function-invocations.js,list-function-logs.js,list-platform-phone-numbers.mjs,list-templates.mjs,list.js,openapi-explore.mjs,publish-flow.js,read-flow-json.js,register-data-endpoint.js,send-interactive.mjs,send-template.mjs,send-test-flow.js,set-data-endpoint.js,setup-encryption.js,submit-template.mjs,template-status.mjs,test.js,update-flow-json.js,update-function.js,update-template.mjs,update.js,upload-media.mjs,upload-template-header-handle.mjs}
|scripts/lib:{args.mjs,cli.js,env.js,env.mjs,http.js,output.js,output.mjs,request.mjs,run.js,whatsapp-flow.js}
|scripts/lib/webhooks:{args.js,kapso-api.js,webhook.js}
```
<!-- FILEMAP:END -->



---

## Referenced Files

> The following files are referenced in this skill and included for context.

### references/getting-started.md

```markdown
---
title: Getting started
description: Enable WhatsApp for your customers
---

Kapso Platform lets your customers connect their own WhatsApp Business accounts without sharing credentials. Each customer uses their own number while you handle the automation.

## Quick example

```javascript
// 1. Create customer
const customer = await createCustomer({ name: 'Acme Corp' });

// 2. Setup link
const setupLink = await generateSetupLink(customer.id);
console.log(`Setup link: ${setupLink.url}`);

// 3. Send message
await sendWhatsAppMessage({
  customer_id: customer.id,
  phone_number: '+1234567890',
  content: 'Order confirmed!'
});
```

## Use cases

Perfect when you need your customers to use their own WhatsApp:

- **CRM platforms** - Let each client connect their WhatsApp for customer communication
- **Appointment booking** - Clinics and salons send reminders from their own number
- **E-commerce tools** - Stores send order updates using their WhatsApp Business
- **Marketing platforms** - Agencies manage multiple client WhatsApp accounts
- **Support software** - Each company provides support through their WhatsApp

## Step 1: Create a customer

```bash
curl -X POST https://api.kapso.ai/platform/v1/customers \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer": {
      "name": "Acme Corporation",
      "external_customer_id": "CUS-12345"
    }
  }'
```

Response:
```json
{
  "data": {
    "id": "customer-abc123",
    "name": "Acme Corporation",
    "external_customer_id": "CUS-12345"
  }
}
```

## Step 2: Generate setup link

```bash
curl -X POST https://api.kapso.ai/platform/v1/customers/customer-abc123/setup_links \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"setup_link":{}}'
```

Response:
```json
{
  "data": {
    "id": "link-xyz789",
    "url": "https://app.kapso.ai/whatsapp/setup/aBcD123...",
    "expires_at": "2024-03-15T10:00:00Z"
  }
}
```

Share the `url` with your customer. They'll click it, log in with Facebook, and connect their WhatsApp in ~5 minutes.

## Step 3: Send messages

After setup completes, use the customer's `phone_number_id` to send messages:

```bash
curl -X POST https://api.kapso.ai/meta/whatsapp/v24.0/110987654321/messages \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "messaging_product": "whatsapp",
    "to": "15551234567",
    "type": "text",
    "text": {
      "body": "Your order has been shipped!"
    }
  }'
```

## JavaScript example

```javascript
const KAPSO_API_KEY = 'YOUR_API_KEY';
const KAPSO_API_BASE_URL = 'https://api.kapso.ai';
const PLATFORM_API_URL = `${KAPSO_API_BASE_URL}/platform/v1`;
const WHATSAPP_API_URL = `${KAPSO_API_BASE_URL}/meta/whatsapp`;

async function onboardCustomer(customerData) {
  // 1. Create customer
  const customer = await fetch(`${PLATFORM_API_URL}/customers`, {
    method: 'POST',
    headers: {
      'X-API-Key': KAPSO_API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ customer: customerData })
  }).then(r => r.json());

  // 2. Generate setup link
  const setupLink = await fetch(
    `${PLATFORM_API_URL}/customers/${customer.data.id}/setup_links`,
    {
      method: 'POST',
      headers: {
        'X-API-Key': KAPSO_API_KEY,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ setup_link: {} })
    }
  ).then(r => r.json());

  return setupLink.data.url;
}

async function sendMessage(phoneNumberId, recipientPhone, message) {
  return fetch(`${WHATSAPP_API_URL}/v24.0/${phoneNumberId}/messages`, {
    method: 'POST',
    headers: {
      'X-API-Key': KAPSO_API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      messaging_product: 'whatsapp',
      to: recipientPhone,
      type: 'text',
      text: { body: message }
    })
  });
}
```

## What customers see

1. Click your setup link
2. Log in with Facebook
3. Connect their WhatsApp Business
4. Verify phone number
5. Done - you can now send messages

The entire process takes ~5 minutes.

## Next steps

- [Setup links](/docs/platform/setup-links) - Customize redirect URLs, branding, connection types
- [Connection detection](/docs/platform/detecting-whatsapp-connection) - Know when customers connect
- [Webhooks](/docs/platform/webhooks/overview) - Handle real-time message events

```

### references/platform-api-reference.md

```markdown
# Kapso Platform API Overview

## Authentication

All Platform API requests require:

```
X-API-Key: <api_key>
```

Base host: `https://api.kapso.ai`
Platform API base path: `/platform/v1`

## Meta proxy (WhatsApp Cloud API)

Base URL: `https://api.kapso.ai/meta/whatsapp/v24.0`

Use Meta proxy for WhatsApp Cloud API calls (messages, templates, media, flows). Auth still uses `X-API-Key`.

## Multi-tenant WhatsApp (Customers)

Use Customers when your end-users connect their own WhatsApp numbers.

Flow:
1. Create customer
2. Create setup link
3. Customer completes embedded signup
4. Use their phone_number_id for sending

Endpoints:
- `POST /customers`
- `GET /customers`
- `GET /customers/:id`
- `POST /customers/:customer_id/setup_links`
- `POST /customers/:customer_id/whatsapp/phone_numbers`
- `GET /whatsapp/phone_numbers?customer_id=<uuid>` (filter phone numbers by customer)

If you are only sending from your own WhatsApp number, skip Customers.

## Core Platform API endpoints

Webhooks:
- Project-level: `GET/POST /whatsapp/webhooks`
- Config-level: `GET/POST /whatsapp/phone_numbers/:id/webhooks`
- Test delivery: `POST /whatsapp/webhooks/:id/test`

Messages and conversations:
- `GET /whatsapp/messages`
- `GET /whatsapp/messages/:id` (WAMID)
- `GET /whatsapp/conversations`
- `GET /whatsapp/conversations/:id`

Message list query params (use `GET /whatsapp/messages`):
- `phone_number_id`, `conversation_id`, `phone_number`
- `direction` (inbound|outbound), `status` (pending|sent|delivered|read|failed)
- `message_type` (text|image|audio|video|document), `has_media` (true|false)
- `page`, `per_page`

Example:
`GET /whatsapp/messages?conversation_id=<uuid>&phone_number_id=<id>&direction=inbound&per_page=50`

Conversation list query params (use `GET /whatsapp/conversations`):
- `phone_number_id`, `phone_number`
- `status` (active|ended)
- `page`, `per_page`

Example:
`GET /whatsapp/conversations?phone_number_id=<id>&status=active&per_page=50`

Workflows:
- `GET /workflows`
- `POST /workflows`
- `GET /workflows/:id`
- `GET /workflows/:id/definition`
- `PATCH /workflows/:id`
- `GET /workflows/:id/variables`
- `GET /workflows/:workflow_id/executions`
- `POST /workflows/:workflow_id/executions`
- `GET /workflow_executions/:id`
- `PATCH /workflow_executions/:id`
- `POST /workflow_executions/:id/resume`
- `GET /workflows/:workflow_id/triggers`
- `POST /workflows/:workflow_id/triggers`
- `PATCH /triggers/:id`
- `DELETE /triggers/:id`

Functions:
- `GET /functions`
- `POST /functions`
- `GET /functions/:id`
- `PATCH /functions/:id`
- `POST /functions/:id/deploy`
- `POST /functions/:id/invoke`
- `GET /functions/:id/invocations`

Integrations:
- `GET /integrations`
- `POST /integrations`
- `PATCH /integrations/:id`
- `DELETE /integrations/:id`
- `GET /integrations/apps`
- `GET /integrations/actions`
- `GET /integrations/accounts`
- `POST /integrations/connect_token`
- `GET /integrations/actions/:action_id/schema`
- `POST /integrations/actions/:action_id/configure_prop`
- `POST /integrations/actions/:action_id/reload_props`

Logs:
- `GET /api_logs`
- `GET /webhook_deliveries`

Provider models:
- `GET /provider_models`

## Guidance

- For template creation/sending, use the Meta proxy endpoints (see `integrate-whatsapp` skill).
- For WhatsApp Flows, use Platform API flow endpoints (see `integrate-whatsapp` skill).
- For workflow graph edits, use workflow definition endpoints (see `automate-whatsapp` skill).

```

### references/setup-links.md

```markdown
---
title: Setup links
description: Customize the WhatsApp onboarding experience
---

Setup links let customers connect their WhatsApp Business accounts to your platform. Send a link, customer clicks, logs in with Facebook, and you're connected.

## Quick start

Create a basic setup link:

```bash
curl -X POST https://api.kapso.ai/platform/v1/customers/{customer_id}/setup_links \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "setup_link": {
      "success_redirect_url": "https://your-app.com/whatsapp/success",
      "failure_redirect_url": "https://your-app.com/whatsapp/failed"
    }
  }'
```

Response includes a `url` you send to your customer. Links expire after 30 days.

See [Connection detection](/docs/platform/detecting-whatsapp-connection) for handling successful connections.

## Connection types

Customers can connect their WhatsApp in two ways:

**Coexistence** - Keep using WhatsApp Business app alongside API
- 5 messages/second
- App stays active
- Good for small businesses

**Dedicated** - API-only access for automation
- Up to 1000 messages/second
- No app access
- Built for scale

## Redirect URLs

Configure where customers land after completing or failing setup:

```json
{
  "setup_link": {
    "success_redirect_url": "https://your-app.com/whatsapp/success",
    "failure_redirect_url": "https://your-app.com/whatsapp/failed"
  }
}
```

Both URLs receive query parameters with setup details. See [Connection detection](/docs/platform/detecting-whatsapp-connection) for handling redirects and available parameters.

## Connection type control

### Show both options (default)
```json
{
  "setup_link": {
    "allowed_connection_types": ["coexistence", "dedicated"]
  }
}
```

### Coexistence only
For customers using WhatsApp Business app:
```json
{
  "setup_link": {
    "allowed_connection_types": ["coexistence"]
  }
}
```

### Dedicated only
For API-only automation:
```json
{
  "setup_link": {
    "allowed_connection_types": ["dedicated"]
  }
}
```

When you provide one option, it auto-selects.

## Theme customization

Match your brand colors:

```json
{
  "setup_link": {
    "theme_config": {
      "primary_color": "#3b82f6",
      "background_color": "#ffffff",
      "text_color": "#1f2937",
      "muted_text_color": "#64748b",
      "card_color": "#f9fafb",
      "border_color": "#e5e7eb"
    }
  }
}
```

All colors use hex format (#RRGGBB).

## Phone number provisioning

Automatically provision a phone number for customers:

```json
{
  "setup_link": {
    "provision_phone_number": true,
    "phone_number_country_isos": ["US"]
  }
}
```

### Country support

- Default: `["US"]` - US phone numbers only
- Non-US countries require custom Twilio credentials (contact sales)

```json
{
  "setup_link": {
    "provision_phone_number": true,
    "phone_number_country_isos": ["US", "CL"]
  }
}
```

## Full example

```javascript
const KAPSO_API_BASE_URL = 'https://api.kapso.ai';
const setupLink = await fetch(
  `${KAPSO_API_BASE_URL}/platform/v1/customers/${customerId}/setup_links`,
  {
    method: 'POST',
    headers: {
      'X-API-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      setup_link: {
        success_redirect_url: 'https://app.example.com/onboarding/complete',
        failure_redirect_url: 'https://app.example.com/onboarding/error',
        allowed_connection_types: ['dedicated'],
        provision_phone_number: true,
        phone_number_country_isos: ['US'],
        theme_config: {
          primary_color: '#10b981',
          background_color: '#ffffff',
          text_color: '#111827'
        }
      }
    })
  }
);

// Send link to customer
await sendEmail(customer.email, {
  subject: 'Connect your WhatsApp',
  body: `Click here to connect: ${setupLink.data.url}`
});
```

## Link management

### List all links
```bash
curl https://api.kapso.ai/platform/v1/customers/{customer_id}/setup_links \
  -H "X-API-Key: YOUR_API_KEY"
```

### Automatic revocation
Creating a new link revokes the previous one. Only one active link per customer.

### Expiration
Links expire after 30 days. Check the `expires_at` field.

```

### references/detecting-whatsapp-connection.md

```markdown
---
title: Connection detection
description: Know when customers complete WhatsApp onboarding
---

You have two ways to detect when customers connect their WhatsApp account through setup links.

## 1. Project webhooks

Configure a project webhook to receive the `whatsapp.phone_number.created` event. This is the recommended approach for server-to-server notifications.

### Setup

1. Open the sidebar and click **Webhooks**
2. Go to the **Project webhooks** tab
3. Click **Add Webhook**
4. Enter your HTTPS endpoint URL
5. Copy the auto-generated secret key
6. Subscribe to `whatsapp.phone_number.created` event

### Webhook payload

```json
{
  "phone_number_id": "123456789012345",
  "project": {
    "id": "990e8400-e29b-41d4-a716-446655440004"
  },
  "customer": {
    "id": "880e8400-e29b-41d4-a716-446655440003"
  }
}
```

### Handle the webhook

```javascript
app.post('/webhooks/project', async (req, res) => {
  const { event, data } = req.body;

  if (event === 'whatsapp.phone_number.created') {
    const { phone_number_id, customer } = data;

    // Update your database
    await db.customers.update(customer.id, {
      phone_number_id,
      whatsapp_connected: true,
      connected_at: new Date()
    });

    // Trigger welcome flow
    await sendWelcomeMessage(phone_number_id, customer.id);
  }

  res.status(200).send('OK');
});
```

See [webhooks documentation](/docs/platform/webhooks) for signature verification and best practices.

## 2. Success redirect URL

When customers complete WhatsApp setup, they're redirected to your `success_redirect_url` with query parameters.

### Setup

When creating a setup link, provide redirect URLs:

```javascript
const KAPSO_API_BASE_URL = 'https://api.kapso.ai';
const setupLink = await fetch(`${KAPSO_API_BASE_URL}/platform/v1/customers/customer-123/setup_links`, {
  method: 'POST',
  headers: {
    'X-API-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    setup_link: {
      success_redirect_url: 'https://your-app.com/whatsapp/success',
      failure_redirect_url: 'https://your-app.com/whatsapp/failed'
    }
  })
});
```

### Query parameters

After successful setup, customer is redirected to:

```
https://your-app.com/whatsapp/success?setup_link_id=...&status=completed&phone_number_id=123456789012345&business_account_id=...&provisioned_phone_number_id=...&display_phone_number=%2B15551234567
```

**Parameters**:
- `setup_link_id` - UUID of the setup link
- `status` - Always `completed` for success
- `phone_number_id` - WhatsApp phone number ID (primary identifier)
- `business_account_id` - Meta WABA ID (if available)
- `provisioned_phone_number_id` - Kapso phone number ID (if provisioning was used)
- `display_phone_number` - E.164 formatted phone number (URL encoded)

### Handle the redirect

```javascript
app.get('/whatsapp/success', async (req, res) => {
  const {
    setup_link_id,
    status,
    phone_number_id,
    business_account_id,
    provisioned_phone_number_id,
    display_phone_number
  } = req.query;

  // Update your database
  await db.customers.update({
    phone_number_id,
    business_account_id,
    display_phone_number: decodeURIComponent(display_phone_number),
    whatsapp_connected: true,
    connected_at: new Date()
  });

  // Show success page to customer
  res.render('whatsapp-connected', {
    phoneNumber: decodeURIComponent(display_phone_number)
  });
});
```

<Note>
These parameters are convenience identifiers to avoid extra API fetches. Use `phone_number_id` as the primary identifier.
</Note>

### Failure redirect

If setup fails, customer is redirected to your `failure_redirect_url`:

```
https://your-app.com/whatsapp/failed?setup_link_id=...&error_code=facebook_auth_failed
```

**Error codes**:
- `facebook_auth_failed` - Facebook login cancelled
- `phone_verification_failed` - Phone verification failed
- `waba_limit_reached` - Too many WhatsApp accounts
- `token_exchange_failed` - OAuth failed
- `link_expired` - Link expired (30 days)
- `already_used` - Link already used

```javascript
app.get('/whatsapp/failed', (req, res) => {
  const { setup_link_id, error_code } = req.query;

  // Log failure for monitoring
  await logSetupFailure(setup_link_id, error_code);

  // Show user-friendly error message
  res.render('whatsapp-setup-failed', {
    errorMessage: getErrorMessage(error_code)
  });
});
```

## Choosing the right method

**Use project webhooks when**:
- You need server-to-server notification
- Customer doesn't need immediate visual feedback
- You're building automated onboarding flows
- You need to process the connection before showing UI

**Use success redirect when**:
- Customer needs immediate confirmation in your app
- You want to show a custom success page
- You're building a wizard-style onboarding flow
- You need to collect additional information after connection

**Use both**:
- Webhook for backend processing (database updates, welcome messages)
- Redirect for frontend experience (success page, next steps)

```

### references/webhooks-overview.md

```markdown
---
title: Webhooks Overview (Kapso)
---

# Webhooks Overview

## Webhook types

### Project webhooks
Project-wide events (for example, `whatsapp.phone_number.created`).
Use **project webhooks** for connection lifecycle and workflow events only.

### WhatsApp webhooks
Message and conversation events for a specific `phone_number_id`.
Use **phone-number webhooks** for `whatsapp.message.*` and `whatsapp.conversation.*` events only.
WhatsApp message events cannot be delivered via project webhooks.

Kinds:

- **Kapso webhooks** (default): event-based payloads, filtering, buffering.
- **Meta webhooks**: raw Meta payloads, no filtering or buffering. One meta webhook per phone number.

Meta webhooks include `X-Idempotency-Key` (SHA256 hash of payload) for deduplication.

## Response requirements

- Your endpoint must return `200 OK` within 10 seconds.
- Non-200 responses trigger retries.

Retry schedule (Kapso webhooks):
- 10 seconds
- 40 seconds
- 90 seconds

## Signature verification

Kapso signs webhook requests:

- Header: `X-Webhook-Signature`
- Value: `HMAC-SHA256(webhook_secret_key, raw_request_body)` as hex

Verify against raw request bytes before parsing JSON.

## Headers (Kapso webhooks)

- `X-Webhook-Event`
- `X-Webhook-Signature`
- `X-Idempotency-Key`
- `X-Webhook-Payload-Version`
- `Content-Type: application/json`

Batched payloads may include:

- `X-Webhook-Batch: true`
- `X-Batch-Size: <n>`

```

### references/webhooks-event-types.md

```markdown
---
title: Event types
description: Available webhook events and their payloads
---

All webhook payloads use v2 format with `phone_number_id` at the top level.

## Payload structure

Webhook payloads separate message data from conversation data:

- **message.kapso** - Message-scoped only: direction, status, processing_status, statuses (raw status history), origin, has_media, content (text representation), transcript (for audio), media helpers (media_data, media_url, message_type_data)
- **conversation** - Top-level identifiers (id, phone_number, phone_number_id). Optional conversation.kapso contains summary metrics (counts, last-message metadata, timestamps)
- **phone_number_id** - Included at top level for routing

## Project webhook events

Use project webhooks for connection lifecycle and workflow events only.

### whatsapp.phone_number.created

Fires when a customer successfully connects their WhatsApp through a setup link.

See [Connection detection](/docs/platform/detecting-whatsapp-connection) for implementation guide.

**Payload**:

```json
{
  "phone_number_id": "123456789012345",
  "project": {
    "id": "990e8400-e29b-41d4-a716-446655440004"
  },
  "customer": {
    "id": "880e8400-e29b-41d4-a716-446655440003"
  }
}
```

### workflow.execution.handoff

Fires when a workflow execution is handed off to a human agent.

**Payload**:

```json
{
  "event": "workflow.execution.handoff",
  "occurred_at": "2025-12-08T12:00:00Z",
  "project_id": "990e8400-e29b-41d4-a716-446655440004",
  "workflow_id": "880e8400-e29b-41d4-a716-446655440001",
  "workflow_execution_id": "770e8400-e29b-41d4-a716-446655440002",
  "status": "handoff",
  "tracking_id": "track-abc123",
  "channel": "whatsapp",
  "whatsapp_conversation_id": "conv_789",
  "handoff": {
    "reason": "User requested human assistance",
    "source": "agent_tool"
  }
}
```

| Field | Description |
|-------|-------------|
| `handoff.reason` | Optional reason provided during handoff |
| `handoff.source` | `agent_tool` (from agent step) or `action_step` (from workflow action) |

### workflow.execution.failed

Fires when a workflow execution fails due to an error.

**Payload**:

```json
{
  "event": "workflow.execution.failed",
  "occurred_at": "2025-12-08T12:00:00Z",
  "project_id": "990e8400-e29b-41d4-a716-446655440004",
  "workflow_id": "880e8400-e29b-41d4-a716-446655440001",
  "workflow_execution_id": "770e8400-e29b-41d4-a716-446655440002",
  "status": "failed",
  "tracking_id": "track-abc123",
  "channel": "whatsapp",
  "whatsapp_conversation_id": "conv_789",
  "error": {
    "message": "Workflow execution timed out"
  }
}
```

## WhatsApp webhook events
Use phone-number webhooks for `whatsapp.message.*` and `whatsapp.conversation.*` events only.

<CardGroup cols={2}>
  <Card title="Message received" icon="message">
    `whatsapp.message.received`

    Fired when a new WhatsApp message is received from a customer. Supports message buffering for batch delivery.
  </Card>
  <Card title="Message sent" icon="paper-plane">
    `whatsapp.message.sent`

    Fired when a message is successfully sent to WhatsApp
  </Card>
  <Card title="Message delivered" icon="check">
    `whatsapp.message.delivered`

    Fired when a message is successfully delivered to the recipient's device
  </Card>
  <Card title="Message read" icon="eye">
    `whatsapp.message.read`

    Fired when the recipient reads your message
  </Card>
  <Card title="Message failed" icon="triangle-exclamation">
    `whatsapp.message.failed`

    Fired when a message fails to deliver
  </Card>
  <Card title="Conversation created" icon="comments">
    `whatsapp.conversation.created`

    Fired when a new WhatsApp conversation is initiated
  </Card>
  <Card title="Conversation ended" icon="clock">
    `whatsapp.conversation.ended`

    Fired when a WhatsApp conversation ends (agent action, manual closure, or 24-hour inactivity)
  </Card>
  <Card title="Conversation inactive" icon="timer">
    `whatsapp.conversation.inactive`

    Fired when no messages (inbound/outbound) for configured minutes (1-1440, default 60)
  </Card>
</CardGroup>

## Payload structures

### whatsapp.message.received

```json
{
  "message": {
    "id": "wamid.123",
    "timestamp": "1730092800",
    "type": "text",
    "text": { "body": "Hello" },
    "kapso": {
      "direction": "inbound",
      "status": "received",
      "processing_status": "pending",
      "origin": "cloud_api",
      "has_media": false,
      "content": "Hello"
    }
  },
  "conversation": {
    "id": "conv_123",
    "phone_number": "+15551234567",
    "status": "active",
    "last_active_at": "2025-10-28T14:25:01Z",
    "created_at": "2025-10-28T13:40:00Z",
    "updated_at": "2025-10-28T14:25:01Z",
    "metadata": {},
    "phone_number_id": "123456789012345",
    "kapso": {
      "contact_name": "John Doe",
      "messages_count": 1,
      "last_message_id": "wamid.123",
      "last_message_type": "text",
      "last_message_timestamp": "2025-10-28T14:25:01Z",
      "last_message_text": "Hello",
      "last_inbound_at": "2025-10-28T14:25:01Z",
      "last_outbound_at": null
    }
  },
  "is_new_conversation": true,
  "phone_number_id": "123456789012345"
}
```

### whatsapp.message.sent

```json
{
  "message": {
    "id": "wamid.456",
    "timestamp": "1730092860",
    "type": "text",
    "text": { "body": "On my way" },
    "kapso": {
      "direction": "outbound",
      "status": "sent",
      "processing_status": "completed",
      "origin": "cloud_api",
      "has_media": false,
      "statuses": [
        {
          "id": "wamid.456",
          "status": "sent",
          "timestamp": "1730092860",
          "recipient_id": "15551234567"
        }
      ]
    }
  },
  "conversation": {
    "id": "conv_123",
    "phone_number": "+15551234567",
    "status": "active",
    "last_active_at": "2025-10-28T14:31:00Z",
    "created_at": "2025-10-28T13:40:00Z",
    "updated_at": "2025-10-28T14:31:00Z",
    "metadata": {},
    "phone_number_id": "123456789012345",
    "kapso": {
      "contact_name": "John Doe",
      "messages_count": 2,
      "last_message_id": "wamid.456",
      "last_message_type": "text",
      "last_message_timestamp": "2025-10-28T14:31:00Z",
      "last_message_text": "On my way",
      "last_inbound_at": "2025-10-28T14:25:01Z",
      "last_outbound_at": "2025-10-28T14:31:00Z"
    }
  },
  "is_new_conversation": false,
  "phone_number_id": "123456789012345"
}
```

### whatsapp.message.delivered

```json
{
  "message": {
    "id": "wamid.456",
    "timestamp": "1730092888",
    "type": "text",
    "text": { "body": "On my way" },
    "kapso": {
      "direction": "outbound",
      "status": "delivered",
      "processing_status": "completed",
      "origin": "cloud_api",
      "has_media": false,
      "statuses": [
        {
          "id": "wamid.456",
          "status": "sent",
          "timestamp": "1730092860",
          "recipient_id": "15551234567"
        },
        {
          "id": "wamid.456",
          "status": "delivered",
          "timestamp": "1730092888",
          "recipient_id": "15551234567"
        }
      ]
    }
  },
  "conversation": {
    "id": "conv_123",
    "phone_number": "+15551234567",
    "status": "active",
    "last_active_at": "2025-10-28T14:31:28Z",
    "created_at": "2025-10-28T13:40:00Z",
    "updated_at": "2025-10-28T14:31:28Z",
    "metadata": {},
    "phone_number_id": "123456789012345"
  },
  "is_new_conversation": false,
  "phone_number_id": "123456789012345"
}
```

### whatsapp.message.failed

```json
{
  "message": {
    "id": "wamid.789",
    "timestamp": "1730093200",
    "type": "text",
    "text": { "body": "This message failed" },
    "kapso": {
      "direction": "outbound",
      "status": "failed",
      "processing_status": "completed",
      "origin": "cloud_api",
      "has_media": false,
      "statuses": [
        {
          "id": "wamid.789",
          "status": "sent",
          "timestamp": "1730093100",
          "recipient_id": "15551234567"
        },
        {
          "id": "wamid.789",
          "status": "failed",
          "timestamp": "1730093200",
          "recipient_id": "15551234567",
          "errors": [
            {
              "code": 131047,
              "title": "Re-engagement message",
              "message": "More than 24 hours have passed since the recipient last replied"
            }
          ]
        }
      ]
    }
  },
  "conversation": {
    "id": "conv_123",
    "phone_number": "+15551234567",
    "status": "active",
    "last_active_at": "2025-10-28T15:00:00Z",
    "created_at": "2025-10-28T13:40:00Z",
    "updated_at": "2025-10-28T15:00:00Z",
    "metadata": {},
    "phone_number_id": "123456789012345"
  },
  "is_new_conversation": false,
  "phone_number_id": "123456789012345"
}
```

### whatsapp.conversation.created

```json
{
  "conversation": {
    "id": "conv_789",
    "phone_number": "+15551234567",
    "status": "active",
    "last_active_at": "2025-10-28T14:00:00Z",
    "created_at": "2025-10-28T14:00:00Z",
    "updated_at": "2025-10-28T14:00:00Z",
    "metadata": {},
    "phone_number_id": "123456789012345",
    "kapso": {
      "contact_name": "John Doe",
      "messages_count": 0,
      "last_message_id": null,
      "last_message_type": null,
      "last_message_timestamp": null,
      "last_message_text": null,
      "last_inbound_at": null,
      "last_outbound_at": null
    }
  },
  "phone_number_id": "123456789012345"
}
```

### whatsapp.conversation.ended

```json
{
  "conversation": {
    "id": "conv_789",
    "phone_number": "+15551234567",
    "status": "ended",
    "last_active_at": "2025-10-28T15:10:45Z",
    "created_at": "2025-10-28T14:00:00Z",
    "updated_at": "2025-10-28T15:10:45Z",
    "metadata": {},
    "phone_number_id": "123456789012345",
    "kapso": {
      "contact_name": "John Doe",
      "messages_count": 15,
      "last_message_id": "wamid.999",
      "last_message_type": "text",
      "last_message_timestamp": "2025-10-28T15:10:45Z",
      "last_message_text": "Thanks!",
      "last_inbound_at": "2025-10-28T15:10:45Z",
      "last_outbound_at": "2025-10-28T15:10:30Z"
    }
  },
  "phone_number_id": "123456789012345"
}
```

### whatsapp.conversation.inactive

```json
{
  "conversation": {
    "id": "conv_789",
    "phone_number": "+15551234567",
    "status": "active",
    "last_active_at": "2025-10-28T13:00:00Z",
    "created_at": "2025-10-28T12:00:00Z",
    "updated_at": "2025-10-28T13:00:00Z",
    "metadata": {},
    "phone_number_id": "123456789012345"
  },
  "since_message": {
    "id": "msg_anchor",
    "whatsapp_message_id": "wamid.ANCHOR",
    "direction": "inbound",
    "created_at": "2025-10-28T13:00:00Z"
  },
  "inactivity": {
    "minutes": 60
  },
  "phone_number_id": "123456789012345"
}
```

### Multiple inactivity timeouts

Create separate webhooks for different timeout thresholds:

```json
// First webhook: 5 minute warning
{
  "events": ["whatsapp.conversation.inactive"],
  "inactive_after_minutes": 5
}

// Second webhook: 30 minute escalation
{
  "events": ["whatsapp.conversation.inactive"],
  "inactive_after_minutes": 30
}
```

Each webhook fires independently when its threshold is reached.

## Message origin

The `message.kapso.origin` field indicates how the message entered the system:

- **cloud_api** - Sent via Kapso API (outbound jobs, flow actions, API calls)
- **business_app** - Echoed from WhatsApp Business App (when using the Business App)
- **history_sync** - Backfilled during message history imports (only if project ran sync)

## Status history

The `message.kapso.statuses` array contains the complete history of raw Meta status events for a message, ordered chronologically. Each entry is the unmodified payload from Meta's webhook.

### Status object structure

Each status object in the array follows Meta's webhook format:

```json
{
  "id": "<WHATSAPP_MESSAGE_ID>",
  "status": "<STATUS>",
  "timestamp": "<UNIX_TIMESTAMP>",
  "recipient_id": "<PHONE_NUMBER>",
  "pricing": {
    "billable": true,
    "pricing_model": "<PRICING_MODEL>",
    "category": "<PRICING_CATEGORY>"
  },
  "errors": [
    {
      "code": 131031,
      "title": "<ERROR_TITLE>",
      "message": "<ERROR_MESSAGE>",
      "error_data": {
        "details": "<ERROR_DETAILS>"
      },
      "href": "<ERROR_CODES_URL>"
    }
  ],
  ...
}
```

| Field | Included when |
|-------|---------------|
| `pricing` | Sent status, plus delivered or read |
| `errors` | Failed to send or deliver |

See [Meta's status webhook reference](https://developers.facebook.com/docs/whatsapp/cloud-api/webhooks/components#statuses-object) for the complete schema.

Use this field to track the full lifecycle of outbound messages and understand failure causes. The array only appears when status events have been recorded.

## Message types

The `message.type` field can be one of:

- `text` - Plain text message
- `image` - Image attachment
- `video` - Video attachment
- `audio` - Audio/voice message
- `document` - Document attachment
- `location` - Location sharing
- `template` - WhatsApp template message
- `interactive` - Interactive message (buttons, lists)
- `reaction` - Message reaction
- `contacts` - Contact card sharing

## Message type-specific data

### Media messages (image/video/document)

```json
{
  "message": {
    "id": "wamid.789",
    "timestamp": "1730093000",
    "type": "image",
    "image": {
      "caption": "Photo description",
      "id": "media_id_123"
    },
    "kapso": {
      "direction": "inbound",
      "status": "received",
      "processing_status": "pending",
      "origin": "cloud_api",
      "has_media": true,
      "content": "Photo description Image attached (photo.jpg) [Size: 200 KB | Type: image/jpeg] URL: https://api.kapso.ai/media/...",
      "media_url": "https://api.kapso.ai/media/...",
      "media_data": {
        "url": "https://api.kapso.ai/media/...",
        "filename": "photo.jpg",
        "content_type": "image/jpeg",
        "byte_size": 204800
      },
      "message_type_data": {
        "caption": "Photo description"
      }
    }
  }
}
```

### Audio messages

```json
{
  "message": {
    "id": "wamid.790",
    "timestamp": "1730093100",
    "type": "audio",
    "audio": {
      "id": "media_id_456"
    },
    "kapso": {
      "direction": "inbound",
      "status": "received",
      "processing_status": "pending",
      "origin": "cloud_api",
      "has_media": true,
      "content": "[Audio attached] (voice.ogg) [Size: 50 KB | Type: audio/ogg] URL: https://api.kapso.ai/media/...\nTranscript: Hello, I need help with my order",
      "transcript": {
        "text": "Hello, I need help with my order"
      },
      "media_url": "https://api.kapso.ai/media/...",
      "media_data": {
        "url": "https://api.kapso.ai/media/...",
        "filename": "voice.ogg",
        "content_type": "audio/ogg",
        "byte_size": 51200
      }
    }
  }
}
```

### Location messages

```json
{
  "message": {
    "type": "location",
    "location": {
      "latitude": 37.7749,
      "longitude": -122.4194,
      "name": "San Francisco",
      "address": "San Francisco, CA, USA"
    }
  }
}
```

### Template messages

```json
{
  "message": {
    "type": "template",
    "template": {
      "name": "order_confirmation",
      "language": {
        "code": "en_US"
      },
      "components": [...]
    }
  }
}
```

### Interactive messages

```json
{
  "message": {
    "type": "interactive",
    "interactive": {
      "type": "button_reply",
      "button_reply": {
        "id": "btn_1",
        "title": "Confirm"
      }
    }
  }
}
```

### Reaction messages

```json
{
  "message": {
    "type": "reaction",
    "reaction": {
      "message_id": "wamid.HBgNNTU0MTIzNDU2Nzg5MA",
      "emoji": "👍"
    }
  }
}
```

```

### references/webhooks-reference.md

```markdown
# Webhook Reference

## Scopes

- Config-level: attach to a specific WhatsApp phone number (use `phone_number_id`).
- Project-level: receive lifecycle/workflow events across all numbers.
- Use config-level for any `whatsapp.message.*` and `whatsapp.conversation.*` events.
- WhatsApp message/conversation events are **not** delivered via project webhooks.

## Signature verification

Kapso signs outbound webhook requests:

- Header: `X-Webhook-Signature`
- Value: `HMAC-SHA256(webhook_secret_key, raw_request_body)` as hex

Verify against the raw request body bytes before JSON parsing.

## Event catalog

Message events (config-level):

- `whatsapp.message.received`
- `whatsapp.message.sent`
- `whatsapp.message.delivered`
- `whatsapp.message.read`
- `whatsapp.message.failed`

Conversation events:

- `whatsapp.conversation.created`
- `whatsapp.conversation.ended`
- `whatsapp.conversation.inactive`

Lifecycle events (project-level only):

- `whatsapp.config.created`
- `whatsapp.phone_number.created`
- `whatsapp.phone_number.deleted`

Workflow events:

- `workflow.execution.handoff`
- `workflow.execution.failed`

## Payload versions

- `v1`: legacy payloads with nested `whatsapp_config`.
- `v2`: modern payloads with `phone_number_id` at root (recommended).

## Buffering (message.received)

Use buffering to batch rapid inbound messages:

- `buffer_enabled`: true
- `buffer_window_seconds`: 1-60
- `max_buffer_size`: 1-100

```

### references/templates-reference.md

```markdown
# WhatsApp Templates via Meta Proxy

## Environment

Required env vars:

- `KAPSO_API_BASE_URL` (host only, no `/platform/v1`, e.g. `https://api.kapso.ai`)
- `KAPSO_API_KEY`
- `META_GRAPH_VERSION` (optional, default: `v24.0`)
- `KAPSO_META_BASE_URL` (optional, defaults to `${KAPSO_API_BASE_URL}/meta/whatsapp`)

## Discover IDs (recommended)

Template CRUD requires `business_account_id` (WABA ID). Sending messages and uploading media require `phone_number_id` (Meta phone number id).

Use the Platform API to discover both:

- Script: `node scripts/list-platform-phone-numbers.mjs`
- Raw: `GET /platform/v1/whatsapp/phone_numbers` (header: `X-API-Key: $KAPSO_API_KEY`)

## Meta proxy endpoints used

- List WABA phone numbers:
  - `GET /{business_account_id}/phone_numbers`
- List templates:
  - `GET /{business_account_id}/message_templates`
- Create template:
  - `POST /{business_account_id}/message_templates`
- Update template:
  - `POST /{business_account_id}/message_templates?hsm_id=<template_id>`
- Delete template (not scripted):
  - `DELETE /{business_account_id}/message_templates?name=<template_name>`
- Send template message:
  - `POST /{phone_number_id}/messages`
- Upload media for send-time headers:
  - `POST /{phone_number_id}/media`

## Template concepts

Categories:
- MARKETING: promotional content.
- UTILITY: transactional updates.
- AUTHENTICATION: OTP/verification (special rules below).

AUTHENTICATION templates:
- Require Meta business verification.
- Body text is fixed by Meta (not customizable).
- Must include an OTP button (COPY_CODE or ONE_TAP).
- Send-time still requires the OTP value in body param {{1}} and URL button param.
- If user wants custom OTP text, use UTILITY instead.

Status flow:
- Kapso does not maintain a separate draft state; create/update calls go to Meta immediately.
- Use `status` from Meta (`APPROVED`, `PENDING`, `REJECTED`, etc) via list/status scripts.

Parameter types:
- POSITIONAL: `{{1}}`, `{{2}}` (sequential).
- NAMED: `{{customer_name}}` (lowercase + underscores). Prefer NAMED.

Component types:
- HEADER (optional)
- BODY (required)
- FOOTER (optional)
- BUTTONS (optional)

## Parameter format (creation time)

Set `parameter_format`:
- `POSITIONAL` (default): `{{1}}`, `{{2}}` with no gaps.
- `NAMED` (recommended): `{{order_id}}`.

## Example requirements (creation time)

If any variables appear in HEADER or BODY, you must include examples:
- POSITIONAL: `example.header_text` and 2D `example.body_text`.
- NAMED: `example.header_text_named_params` and `example.body_text_named_params`.

## Components cheat sheet (creation time)

### Header (TEXT, named)

```json
{
  "type": "HEADER",
  "format": "TEXT",
  "text": "Sale starts {{sale_date}}",
  "example": {
    "header_text_named_params": [
      { "param_name": "sale_date", "example": "December 1" }
    ]
  }
}
```

### Header (TEXT, positional)

```json
{
  "type": "HEADER",
  "format": "TEXT",
  "text": "Sale starts {{1}}",
  "example": {
    "header_text": ["December 1"]
  }
}
```

### Header (IMAGE/VIDEO/DOCUMENT)

```json
{
  "type": "HEADER",
  "format": "IMAGE",
  "example": {
    "header_handle": ["<header_handle>"]
  }
}
```

### Body (named)

```json
{
  "type": "BODY",
  "text": "Hi {{customer_name}}, order {{order_id}} is ready.",
  "example": {
    "body_text_named_params": [
      { "param_name": "customer_name", "example": "Alex" },
      { "param_name": "order_id", "example": "ORDER-123" }
    ]
  }
}
```

### Body (positional)

```json
{
  "type": "BODY",
  "text": "Order {{1}} is ready for {{2}}.",
  "example": {
    "body_text": [["ORDER-123", "Alex"]]
  }
}
```

### Footer (no variables)

```json
{
  "type": "FOOTER",
  "text": "Reply STOP to opt out"
}
```

### Buttons

```json
{
  "type": "BUTTONS",
  "buttons": [
    { "type": "QUICK_REPLY", "text": "Need help" },
    { "type": "URL", "text": "Track", "url": "https://example.com/track?id={{1}}", "example": ["https://example.com/track?id=ORDER-123"] }
  ]
}
```

Button ordering rules:
- Do not interleave QUICK_REPLY with URL/PHONE_NUMBER.
- Valid: QUICK_REPLY, QUICK_REPLY, URL, PHONE_NUMBER
- Invalid: QUICK_REPLY, URL, QUICK_REPLY
- Dynamic URL variables must be at the end of the URL.

URL button variables use positional placeholders in the URL (for example `{{1}}`). At send-time, include a `button` component with `sub_type: "url"` and the correct `index`.

Example (send-time URL button param):

```json
{
  "type": "button",
  "sub_type": "url",
  "index": "0",
  "parameters": [{ "type": "text", "text": "ORDER-123" }]
}
```

## AUTHENTICATION components

```json
{
  "type": "BODY",
  "add_security_recommendation": true,
  "code_expiration_minutes": 10
}
```

```json
{
  "type": "BUTTONS",
  "buttons": [
    { "type": "OTP", "otp_type": "COPY_CODE", "text": "Copy code" }
  ]
}
```

## Send-time components

Named parameters:

```json
{
  "type": "body",
  "parameters": [
    { "type": "text", "parameter_name": "order_id", "text": "ORDER-123" }
  ]
}
```

Positional parameters:

```json
{
  "type": "body",
  "parameters": [
    { "type": "text", "text": "ORDER-123" }
  ]
}
```

AUTHENTICATION send-time:

```json
[
  {
    "type": "body",
    "parameters": [{ "type": "text", "text": "123456" }]
  },
  {
    "type": "button",
    "sub_type": "url",
    "index": "0",
    "parameters": [{ "type": "text", "text": "123456" }]
  }
]
```

Media header send-time (use id or link, not both):

```json
{
  "type": "header",
  "parameters": [
    { "type": "image", "image": { "id": "4490709327384033" } }
  ]
}
```

## Header handle limitation

The Meta proxy does not expose resumable upload endpoints for `header_handle`. Use Platform media ingest (`/platform/v1/whatsapp/media` with `delivery: meta_resumable_asset`) if a header_handle is required.

```json
{
  "type": "header",
  "parameters": [
    { "type": "image", "image": { "link": "https://example.com/header.jpg" } }
  ]
}
```

Rules:

- Use either `id` or `link` (never both).
- Always include the header component when the template has a media header.

```

### references/whatsapp-api-reference.md

```markdown
---
title: WhatsApp Cloud API via Kapso Proxy
---

# WhatsApp Cloud API (Kapso Meta Proxy)

REST API reference for sending messages, managing templates, and querying history via Kapso's Meta proxy.

## Base URL and auth

```
Base URL: ${KAPSO_API_BASE_URL}/meta/whatsapp/v24.0
Auth header: X-API-Key: <api_key>
```

All payloads mirror the Meta Cloud API. Kapso adds storage and query features.

## Send messages

`POST /{phone_number_id}/messages`

All payloads require `messaging_product: "whatsapp"`.

### Text

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "text",
  "text": { "body": "Hello!", "preview_url": true }
}
```

### Image

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "image",
  "image": { "link": "https://example.com/photo.jpg", "caption": "Photo" }
}
```

Use `id` instead of `link` for uploaded media.

### Video

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "video",
  "video": { "link": "https://example.com/clip.mp4", "caption": "Video" }
}
```

### Audio

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "audio",
  "audio": { "link": "https://example.com/audio.mp3" }
}
```

### Voice message

Voice messages require `.ogg` files with OPUS codec. Set `voice: true`:

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "audio",
  "audio": { "id": "<MEDIA_ID>", "voice": true }
}
```

### Document

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "document",
  "document": { "link": "https://example.com/file.pdf", "filename": "report.pdf", "caption": "Report" }
}
```

### Sticker

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "sticker",
  "sticker": { "id": "<MEDIA_ID>" }
}
```

### Location

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "location",
  "location": { "latitude": 37.7749, "longitude": -122.4194, "name": "SF Office", "address": "123 Main St" }
}
```

### Contacts

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "contacts",
  "contacts": [{
    "name": { "formatted_name": "John Doe", "first_name": "John", "last_name": "Doe" },
    "phones": [{ "phone": "+15551234567", "type": "MOBILE", "wa_id": "15551234567" }],
    "emails": [{ "email": "[email protected]", "type": "WORK" }]
  }]
}
```

### Reaction

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "reaction",
  "reaction": { "message_id": "wamid......", "emoji": "👍" }
}
```

Note: Reactions only trigger `sent` status webhook (not delivered/read).

### Reply to a message

Add `context` to reply to a specific message:

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "text",
  "context": { "message_id": "wamid......" },
  "text": { "body": "Thanks for your message!" }
}
```

## Interactive messages

Require an active 24-hour session window. Use templates for outbound notifications outside the window.

### Buttons

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "interactive",
  "interactive": {
    "type": "button",
    "body": { "text": "Choose an option" },
    "action": {
      "buttons": [
        { "type": "reply", "reply": { "id": "yes", "title": "Yes" } },
        { "type": "reply", "reply": { "id": "no", "title": "No" } }
      ]
    }
  }
}
```

Max 3 buttons. Button titles max 20 chars.

### List

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "interactive",
  "interactive": {
    "type": "list",
    "header": { "type": "text", "text": "Shipping Options" },
    "body": { "text": "Choose your preferred shipping" },
    "footer": { "text": "Estimates may vary" },
    "action": {
      "button": "View Options",
      "sections": [{
        "title": "Fast",
        "rows": [
          { "id": "express", "title": "Express", "description": "1-2 days" },
          { "id": "priority", "title": "Priority", "description": "2-3 days" }
        ]
      }]
    }
  }
}
```

Max 10 sections, 10 rows total. Button text max 20 chars.

### CTA URL

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "interactive",
  "interactive": {
    "type": "cta_url",
    "body": { "text": "Track your order" },
    "action": {
      "name": "cta_url",
      "parameters": { "display_text": "Track Order", "url": "https://example.com/track/123" }
    }
  }
}
```

### Location request

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "interactive",
  "interactive": {
    "type": "location_request_message",
    "body": { "text": "Please share your location for delivery." },
    "action": { "name": "send_location" }
  }
}
```

### Flow

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "interactive",
  "interactive": {
    "type": "flow",
    "header": { "type": "text", "text": "Book Appointment" },
    "body": { "text": "Schedule your visit" },
    "action": {
      "name": "flow",
      "parameters": {
        "flow_message_version": "3",
        "flow_id": "123456789",
        "flow_cta": "Book Now",
        "mode": "published",
        "flow_token": "session_abc123",
        "flow_action": "navigate",
        "flow_action_payload": {
          "screen": "WELCOME_SCREEN",
          "data": { "customer_id": "cust_123" }
        }
      }
    }
  }
}
```

### Product

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "interactive",
  "interactive": {
    "type": "product",
    "body": { "text": "Check out this item" },
    "action": {
      "catalog_id": "CATALOG_ID",
      "product_retailer_id": "SKU_1234"
    }
  }
}
```

### Product list

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "interactive",
  "interactive": {
    "type": "product_list",
    "header": { "type": "text", "text": "Our Bestsellers" },
    "body": { "text": "Choose a product" },
    "action": {
      "catalog_id": "CATALOG_ID",
      "sections": [{
        "title": "Popular",
        "product_items": [
          { "product_retailer_id": "SKU_1234" },
          { "product_retailer_id": "SKU_2345" }
        ]
      }]
    }
  }
}
```

Max 10 sections, 30 products total.

### Catalog message

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "interactive",
  "interactive": {
    "type": "catalog_message",
    "body": { "text": "Browse our catalog." },
    "action": {
      "name": "catalog_message",
      "parameters": { "thumbnail_product_retailer_id": "SKU_THUMBNAIL" }
    }
  }
}
```

## Template messages

### Send with named parameters

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "template",
  "template": {
    "name": "order_confirmation",
    "language": { "code": "en_US" },
    "components": [{
      "type": "body",
      "parameters": [
        { "type": "text", "parameter_name": "customer_name", "text": "Jessica" },
        { "type": "text", "parameter_name": "order_number", "text": "ORD-12345" }
      ]
    }]
  }
}
```

### Send with positional parameters

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "template",
  "template": {
    "name": "order_confirmation",
    "language": { "code": "en_US" },
    "components": [{
      "type": "body",
      "parameters": [
        { "type": "text", "text": "Jessica" },
        { "type": "text", "text": "ORD-12345" }
      ]
    }]
  }
}
```

### Send with media header

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "template",
  "template": {
    "name": "seasonal_promotion",
    "language": { "code": "en_US" },
    "components": [
      {
        "type": "header",
        "parameters": [{ "type": "image", "image": { "link": "https://example.com/promo.jpg" } }]
      },
      {
        "type": "body",
        "parameters": [
          { "type": "text", "parameter_name": "sale_name", "text": "Summer Sale" },
          { "type": "text", "parameter_name": "discount", "text": "25%" }
        ]
      }
    ]
  }
}
```

### Send with URL button variable

```json
{
  "messaging_product": "whatsapp",
  "to": "15551234567",
  "type": "template",
  "template": {
    "name": "order_tracking",
    "language": { "code": "en_US" },
    "components": [
      {
        "type": "body",
        "parameters": [{ "type": "text", "parameter_name": "order_id", "text": "ORD-123" }]
      },
      {
        "type": "button",
        "sub_type": "url",
        "index": "0",
        "parameters": [{ "type": "text", "text": "ORD-123" }]
      }
    ]
  }
}
```

See [templates-reference.md](templates-reference.md) for full component rules.

## Template CRUD

### List templates

`GET /{business_account_id}/message_templates`

| Param | Description |
|-------|-------------|
| `name` | Filter by template name |
| `status` | `APPROVED`, `PENDING`, `REJECTED` |
| `category` | `AUTHENTICATION`, `MARKETING`, `UTILITY` |
| `language` | Language code (e.g., `en_US`) |
| `limit` | Max 100 |

### Create template

`POST /{business_account_id}/message_templates`

```json
{
  "name": "order_confirmation",
  "language": "en_US",
  "category": "UTILITY",
  "parameter_format": "NAMED",
  "components": [
    {
      "type": "BODY",
      "text": "Thank you, {{customer_name}}! Your order {{order_number}} is confirmed.",
      "example": {
        "body_text_named_params": [
          { "param_name": "customer_name", "example": "Pablo" },
          { "param_name": "order_number", "example": "ORD-12345" }
        ]
      }
    }
  ]
}
```

Response includes `id` and `status` (usually `PENDING`).

### Update template

`PUT /{business_account_id}/message_templates?hsm_id=<template_id>`

Same body structure as create.

### Delete template

`DELETE /{business_account_id}/message_templates?name=<name>` or `?hsm_id=<template_id>`

## Mark as read

`POST /{phone_number_id}/messages`

```json
{
  "messaging_product": "whatsapp",
  "status": "read",
  "message_id": "wamid......"
}
```

### With typing indicator

```json
{
  "messaging_product": "whatsapp",
  "status": "read",
  "message_id": "wamid......",
  "typing_indicator": { "type": "text" }
}
```

Typing indicator dismisses on send or after ~25s.

## Media

### Upload

`POST /{phone_number_id}/media` (multipart/form-data)

| Field | Value |
|-------|-------|
| `file` | Binary file |
| `messaging_product` | `whatsapp` |

Returns `{ "id": "<MEDIA_ID>" }` for use in send payloads.

### Get URL

`GET /{media_id}?phone_number_id=<phone_number_id>`

Returns temporary download URL (valid 5 minutes).

### Delete

`DELETE /{media_id}?phone_number_id=<phone_number_id>`

### Format limits

| Type | Formats | Max Size |
|------|---------|----------|
| Image | JPEG, PNG | 5 MB |
| Video | MP4, 3GP (H.264 + AAC) | 16 MB |
| Audio | AAC, AMR, MP3, M4A, OGG | 16 MB |
| Voice | OGG (OPUS codec only) | 16 MB |
| Document | PDF, DOC, DOCX, PPT, PPTX, XLS, XLSX, TXT | 100 MB |
| Sticker (static) | WEBP | 100 KB |
| Sticker (animated) | WEBP | 500 KB |

## Query history (Kapso)

These endpoints are Kapso-specific for stored conversation data.

### List messages

`GET /{phone_number_id}/messages`

| Param | Description |
|-------|-------------|
| `conversation_id` | Filter by conversation UUID |
| `direction` | `inbound` or `outbound` |
| `status` | `pending`, `sent`, `delivered`, `read`, `failed` |
| `since` / `until` | ISO 8601 timestamps |
| `limit` | Max 100 |
| `before` / `after` | Cursor pagination |
| `fields` | Use `kapso(...)` for extra fields |

### List conversations

`GET /{phone_number_id}/conversations`

| Param | Description |
|-------|-------------|
| `status` | `active` or `ended` |
| `last_active_since` / `last_active_until` | ISO 8601 timestamps |
| `phone_number` | Filter by customer phone (E.164) |
| `limit` | Max 100 |
| `before` / `after` | Cursor pagination |
| `fields` | Use `kapso(...)` for extra fields |

### Get conversation

`GET /{phone_number_id}/conversations/{conversation_id}`

### List contacts

`GET /{phone_number_id}/contacts`

| Param | Description |
|-------|-------------|
| `wa_id` | Filter by WhatsApp ID |
| `customer_id` | Filter by associated customer |
| `has_customer` | `true` or `false` |
| `limit` | Max 100 |
| `before` / `after` | Cursor pagination |

### Get contact

`GET /{phone_number_id}/contacts/{wa_id}`

## Response format

Successful send returns:

```json
{
  "messaging_product": "whatsapp",
  "contacts": [{ "input": "15551234567", "wa_id": "15551234567" }],
  "messages": [{ "id": "wamid.HBgN..." }]
}
```

## Errors

| Code | Description |
|------|-------------|
| 131047 | 24-hour window expired. Use template instead. |
| 1026 | Receiver incapable (e.g., address_message not supported) |
| 409 | Another message in-flight for this conversation. Retry shortly. |

## Kapso extensions

Add `fields=kapso(...)` to list endpoints:

- `kapso(default)` or `kapso(*)` - all default fields
- `kapso(direction,media_url,contact_name)` - specific fields
- `kapso()` - omit Kapso fields

Common fields: `direction`, `status`, `media_url`, `contact_name`, `flow_response`, `flow_token`, `content`, `message_type_data`.

## Notes

- Discover `phone_number_id` + `business_account_id` via `node scripts/list-platform-phone-numbers.mjs`
- All send payloads require `messaging_product: "whatsapp"`
- Graph version controlled by `META_GRAPH_VERSION` (default `v24.0`)

```

### references/whatsapp-cloud-api-js.md

```markdown
---
title: whatsapp-cloud-api-js SDK
---

# whatsapp-cloud-api-js

Use the `@kapso/whatsapp-cloud-api` SDK for typed WhatsApp Cloud API calls.

## Install

```bash
npm install @kapso/whatsapp-cloud-api
```

## Create a client

Kapso proxy setup:

```ts
import { WhatsAppClient } from "@kapso/whatsapp-cloud-api";

const client = new WhatsAppClient({
  baseUrl: "https://api.kapso.ai/meta/whatsapp",
  kapsoApiKey: process.env.KAPSO_API_KEY!
});
```

Direct Meta setup:

```ts
const client = new WhatsAppClient({
  accessToken: process.env.WHATSAPP_TOKEN!
});
```

## Send a text message

```ts
await client.messages.sendText({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  to: "+15551234567",
  body: "Hello from Kapso"
});
```

## Send a template message

```ts
await client.messages.sendTemplate({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  to: "+15551234567",
  template: {
    name: "order_ready_named",
    language: { code: "en_US" },
    components: [
      {
        type: "body",
        parameters: [
          { type: "text", parameterName: "order_id", text: "ORDER-123" }
        ]
      }
    ]
  }
});
```

## Send an interactive button message

```ts
await client.messages.sendInteractiveButtons({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  to: "+15551234567",
  bodyText: "Choose an option",
  buttons: [
    { id: "accept", title: "Accept" },
    { id: "decline", title: "Decline" }
  ]
});
```

## List conversations

```ts
const conversations = await client.conversations.list({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  status: "active",
  limit: 20
});
```

## Get a conversation

```ts
const conversation = await client.conversations.get({
  conversationId: "123e4567-e89b-12d3-a456-426614174000"
});
```

## List messages

```ts
const messages = await client.messages.query({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  conversationId: "123e4567-e89b-12d3-a456-426614174000",
  limit: 50
});
```

## List messages for a conversation (shortcut)

```ts
const messages = await client.messages.listByConversation({
  phoneNumberId: "<PHONE_NUMBER_ID>",
  conversationId: "123e4567-e89b-12d3-a456-426614174000",
  limit: 50
});
```

## Notes

- Use `phoneNumberId` from the connected WhatsApp number (discover via `node scripts/list-platform-phone-numbers.mjs`).
- With Kapso proxy, keep `baseUrl` and `kapsoApiKey` set.
- Template rules still apply (examples, button ordering, media headers).
- History endpoints (`messages.query`, `messages.listByConversation`, `conversations.list/get`) require Kapso proxy; they are not available with a direct Meta access token.
- Requests use camelCase keys and the SDK converts to snake_case for the API; responses come back camelCase.

```

### references/whatsapp-flows-spec.md

```markdown
# WhatsApp Business Platform: Flow JSON Reference

## IMPORTANT: Version Requirements

**Always use Flow JSON version 7.3** (the current recommended version). Earlier versions like 5.0 are no longer supported for publishing flows.

```json
{
  "version": "7.3",
  "data_api_version": "3.0",
  ...
}
```

---

Flow JSON enables businesses to create workflows in WhatsApp by accessing the features of WhatsApp Flows using a custom JSON object developed by Meta. These workflows are initiated, run, and managed entirely inside WhatsApp. They can include multiple screens, data flows, and response messages.

To visualize the complete user experience, use the **Builder** in the WhatsApp Manager (Account tools > Flows), which emulates the entire Flow experience.

## Structure of Flow JSON

Flow JSON consists of the following sections:

| Section               | Description                                                                                                                              |
| :-------------------- | :--------------------------------------------------------------------------------------------------------------------------------------- |
| **Screen Data Model** | Commands to define static types that power the screen.                                                                                   |
| **Screens**           | Used to compose layouts using standard UI library components.                                                                            |
| **Components**        | Individual building blocks that make up a screen (text fields, buttons, etc.).                                                           |
| **Routing Model**     | Defines the rules for screen transitions (e.g., Screen 1 can only go to Screen 2). Used for validation.                                  |
| **Actions**           | Syntax to invoke client-side logic. Allowed actions: `navigate`, `data_exchange`, `complete`, `open_url` (v6.0+), `update_data` (v6.0+). |

## Data Exchange quick spec (Kapso Functions)
- WhatsApp sends (POST): `action` (`INIT`/`data_exchange`/`BACK`), `screen`, `data` (fields), `flow_token`, optional `flow_token_signature` (JWT with Meta app secret). Expect response in ~10–15s.
- Kapso forwards to your Function as JSON: `{ source: "whatsapp_flow", flow: { id, meta_flow_id }, data_exchange: <original payload>, signature_valid: true|false, received_at: "<iso8601>" }`.
- Your Function must return JSON:
  - Next screen: `{ "screen": "NEXT_SCREEN", "data": { ... } }`
  - Validation on same screen: `{ "screen": "CURRENT", "data": { "error_message": "Try again" } }`
  - Completion: `{ "screen": "SUCCESS", "data": { "extension_message_response": { "params": { ... } } } }`
  - Optional: include `vars` to persist variables; `next_edge` is not required for WhatsApp flows.
- Do NOT include `endpoint_uri` / `data_channel_uri` (Kapso injects). Do NOT wrap JSON in markdown fences.
- Runtime tips: keep total work under ~12s, minimal external calls, respond with `Content-Type: application/json`.

### Common gotchas (read me first)
- **Dynamic property rule:** `${form.*}` and `${data.*}` must be the entire property value. `"Hello ${data.name}"` is invalid unless you use a backticked nested expression (v6.0+): ``"text": "`'Hello ' ${data.name}`"``.
- **Schema as whitelist:** On dynamic screens, declare **every** field your endpoint may return under `screen.data`. Undeclared fields may be dropped by Meta/Kapso even if your endpoint returns them.
- **Custom payload keys:** Extra keys in action payloads are forwarded as-is to `data_exchange.data`. Avoid reserved names like `action`, `screen`, `data`; prefer explicit names such as `user_action`, `step`, `intent`.
- **Version is mandatory:** Every endpoint response must include `"version": "3.0"` or the UI can appear to return empty data (silent failure).
- **Error responses need full data:** When you stay on the same screen with `error_message`, still include all data the screen expects (lists, init-values, etc.) or components can break.
- **Carry data forward:** Global references (`${screen.SCREEN.form.field}`) exist, but the safest pattern is to re-send needed values in each response: return them in `data` for the next screen and reference via `${data.field}`.
- **Routing pattern:** Route primarily on `data_exchange.action` then `data_exchange.screen`; see the example in “Action Routing Pattern” below for a multi-screen skeleton.

### Mini handler examples
Cloudflare Worker:
```javascript
async function handler(request) {
  const body = await request.json();
  const screen = body.data_exchange?.screen || "WELCOME";
  return new Response(JSON.stringify({
    screen: "SUCCESS",
    data: { message: `Thanks from ${screen}` }
  }), { headers: { "Content-Type": "application/json" } });
}
```

## Data Endpoint Quick Start (Kapso Functions)

This section is the **minimum you must do** to make a WhatsApp Flow endpoint work reliably with Kapso Functions.

### Enabling flows encryption
- Endpoint operations (create/update) require Flows encryption to be enabled.
- The Kapso Agent can enable this for you automatically when needed.
- If the agent can’t enable it (for example, no phone number selected) or asks you to do it manually:
  - In Kapso, click the **Settings** gear in the top-right toolbar.
  - Open the WhatsApp/Phone configuration section.
  - Toggle **Enable encryption** for the phone number / WhatsApp Business Account.

### 1. Response Contract (Critical)

Every response from your Function **must** include all three fields:

```json
{
  "version": "3.0",
  "screen": "NEXT_SCREEN_ID",
  "data": { }
}
```

- **CRITICAL:** if `version` is missing or incorrect, the flow can appear to
  succeed at the HTTP layer but behave as if no data was returned (silent
  failure / empty data in the UI).

- `version`
  - **Required** for all `data_exchange` responses.
  - Use `"3.0"` unless Meta changes the `data_api_version`.
  - If you omit or mis-set this, the flow may **fail silently** and appear to return empty data.
- `screen`
  - The screen to render next. Must exist in the Flow’s `routing_model`.
- `data`
  - Object with properties consumed by your Flow JSON (lists, error messages, etc.).

Examples:

```json
{
  "version": "3.0",
  "screen": "APPOINTMENT_SLOTS",
  "data": {
    "available_slots": [
      { "id": "slot-1", "title": "Today 10:00" },
      { "id": "slot-2", "title": "Today 11:00" }
    ]
  }
}
```

```json
{
  "version": "3.0",
  "screen": "APPOINTMENT_SLOTS",
  "data": {
    "error_message": "Please select a valid time",
    "available_slots": [
      { "id": "slot-1", "title": "Today 10:00" },
      { "id": "slot-2", "title": "Today 11:00" }
    ]
  }
}
```

```json
{
  "version": "3.0",
  "screen": "SUCCESS",
  "data": {
    "extension_message_response": {
      "params": {
        "flow_token": "<FLOW_TOKEN>",
        "summary": "Booked for 2025-01-10 at 10:00"
      }
    }
  }
}
```

### 2. Request Payload Map

Kapso decrypts Meta’s encrypted payload and forwards the following JSON to your Function:

```json
{
  "source": "whatsapp_flow",
  "flow": {
    "id": "<kapso_flow_id>",
    "meta_flow_id": "<meta_flow_id>"
  },
  "data_exchange": {
    "version": "3.0",
    "action": "INIT",
    "screen": "CURRENT_SCREEN_ID",
    "data": {},
    "flow_token": "<FLOW_TOKEN>",
    "flow_token_signature": "<JWT>"
  },
  "signature_valid": true,
  "received_at": "2025-01-01T12:34:56Z"
}
```

In your handler:

- User inputs: `body.data_exchange.data.<field_name>`
- Current screen: `body.data_exchange.screen`
- Flow action: `body.data_exchange.action` (`"INIT"`, `"data_exchange"`, `"BACK"`)
- Flow token: `body.data_exchange.flow_token`
- Signature: `body.data_exchange.flow_token_signature` + `body.signature_valid` if you want to enforce it.

You **do not** need to implement encryption/decryption yourself; Kapso already does that before calling your Function.

Note: in invocation logs we store a simplified shape with a top-level
`flow_id` for convenience. The JSON sent to your Function uses the `flow`
object as shown above.

### 3. Action Routing Pattern

A simple decision tree that works for most flows:

- **Best practice:** route primarily by `data_exchange.action` and
  `data_exchange.screen`. Use custom fields inside `data` only for secondary
  branching, not as your main router.

```ts
const de = body.data_exchange;
const action = de.action;
const screen = de.screen;
const data = de.data || {};

if (action === "INIT") {
  // First time the flow calls your endpoint
  return {
    version: "3.0",
    screen: "FIRST_SCREEN",
    data: {}
  };
}

if (action === "BACK") {
  // User pressed back; decide whether to re-load or reuse previous data
  return {
    version: "3.0",
    screen: screen || "FIRST_SCREEN",
    data: {}
  };
}

if (action === "data_exchange") {
  if (screen === "SCREEN_A") {
    // Process Screen A and send Screen B
    return {
      version: "3.0",
      screen: "SCREEN_B",
      data
    };
  }

  if (screen === "SCREEN_B") {
    // Maybe complete the flow
    return {
      version: "3.0",
      screen: "SUCCESS",
      data: {
        extension_message_response: {
          params: {
            flow_token: de.flow_token
          }
        }
      }
    };
  }
}
```

End-to-end multi-screen skeleton (INIT → SCREEN_A → SCREEN_B → SUCCESS):

```ts
if (action === "INIT") {
  return { version: "3.0", screen: "SCREEN_A", data: {} };
}

if (action === "data_exchange") {
  if (screen === "SCREEN_A") {
    // validate, fetch data
    return { version: "3.0", screen: "SCREEN_B", data: { slots, user_email: data.email } };
  }

  if (screen === "SCREEN_B") {
    // final submission
    return {
      version: "3.0",
      screen: "SUCCESS",
      data: {
        extension_message_response: {
          params: { flow_token: de.flow_token }
        }
      }
    };
  }
}
```

You can also use a custom field inside `data` (for example, `data.action`) to branch when multiple buttons on the same screen go to different places.

When in doubt, start with a minimal pattern:

- On `INIT`, return your first screen with any initial data.
- On `data_exchange` from that screen, validate inputs and either:
  - stay on the same screen with `error_message`, or
  - move to the next screen with derived data.
- On `data_exchange` from the final screen, return `SUCCESS` with an
  `extension_message_response` payload to complete the flow.

### 4. Error Handling Rules

Follow Meta’s rules, but in practice:

- Always return HTTP `200` from your Function (Kapso handles HTTP errors upstream).
- Never rely on 4xx/5xx to signal validation errors to the user.
- Use `data.error_message` with the same `screen` to show a snackbar and keep the user on the current screen.
- When returning an error, still include all data fields the screen expects (lists, init values, etc.) so components continue to render.
- Keep payloads small; avoid huge objects or unnecessary fields.

Example (validation failure):

```json
{
  "version": "3.0",
  "screen": "APPOINTMENT_SLOTS",
  "data": {
    "error_message": "That time is no longer available. Please pick another.",
    "available_slots": []
  }
}
```

### 5. Component Data Contract (Dynamic Options)

When a component uses a data source from the endpoint, the field name and shape must match exactly.

Example – dropdown with dynamic options:

```json
{
  "type": "Dropdown",
  "name": "slot_id",
  "data-source": "${data.available_slots}"
}
```

Your endpoint must return:

```json
{
  "version": "3.0",
  "screen": "APPOINTMENT_SLOTS",
  "data": {
    "available_slots": [
      { "id": "slot-1", "title": "Today 10:00" },
      { "id": "slot-2", "title": "Today 11:00" }
    ]
  }
}
```

- Field name in `data` → `available_slots`
- Each item → `{ "id": string, "title": string }`

### 6. Debugging & Logging Checklist

When debugging a Flow + Endpoint integration:

1. Log the raw forwarded body (from Kapso to your Function).
2. Log:
   - `data_exchange.action`
   - `data_exchange.screen`
   - `data_exchange.data`
3. Log the external API request/response (status, body shape).
4. Log the final response you return to Kapso (with `version`, `screen`, `data`).
5. If the UI shows an empty screen or no data:
   - Check `version` is `"3.0"` in responses.
   - Check `screen` exists in `routing_model`.
   - Check `data` field names match the Flow JSON.
   - Check array/object shape (especially for lists).

---

## Top-level Flow JSON Properties

### Required Properties
*   **`version`**: Represents the version of Flow JSON to use during compilation.
*   **`screens`**: An array representing the different pages (nodes) of the user experience.

### Optional Properties
*   **`routing_model`**: Represents the routing system. Generated automatically if the Flow does not use a Data Endpoint. Required if using an endpoint.
*   **`data_api_version`**: The version used for communication with the WhatsApp Flows Data Endpoint (currently `3.0`).
*   **`data_channel_uri`**: **Note:** Not supported as of Flow JSON 3.0. For v3.0+, configure the URL using the `endpoint_uri` field provided by the Flows API.

> **Version fields cheat sheet**
>
> - Flow JSON `version`: which Flow JSON schema version your flow uses (for example, `"3.1"`).
> - `data_api_version`: protocol version for the data endpoint (currently `"3.0"`).
> - Data-exchange payload `version`: the value Meta sends in `data_exchange.version` (should match `data_api_version`).
> - Endpoint response `version`: you must echo `"3.0"` in every response your Function returns. If you omit it, Meta may treat the payload as invalid and your flow can appear to return empty data.

**Example (Version 2.1 - Legacy)**
```json
{
 "version": "2.1",
 "data_api_version": "3.0",
 "routing_model": {"MY_FIRST_SCREEN": ["MY_SECOND_SCREEN"] },
 "screens": [...],
 "data_channel_uri": "https://example.com"
}
```

**Example (Version 3.1 - Current)**
```json
{
 "version": "3.1",
 "data_api_version": "3.0",
 "routing_model": {"MY_FIRST_SCREEN": ["MY_SECOND_SCREEN"] },
 "screens": [...]
}
```

---

## Screens

Screens are the main unit of a Flow. Each screen represents a single node in the state machine.

**Schema:**
```json
"screen" : {
  "id": "string",
  "terminal": boolean,
  "success": boolean,
  "title": "string",
  "refresh_on_back": boolean,
  "data": object,
  "layout": object,
  "sensitive": []
}
```

### Properties
*   **`id`** (Required): Unique identifier. `SUCCESS` is a reserved keyword and cannot be used.
*   **`layout`** (Required): The UI Layout. Can be predefined or a container with custom content using the WhatsApp Flows Library.
*   **`terminal`** (Optional): If `true`, this screen ends the flow. A Footer component is mandatory on terminal screens.
*   **`title`** (Optional): Attribute rendered in the top navigation bar.
*   **`success`** (Optional, terminal only): Defaults to `true`. Marks whether terminating on this screen is a successful business outcome.
*   **`data`** (Optional): Declaration of dynamic data that fills the components using JSON Schema.
    *   **Dynamic screens:** Declare **every field** your endpoint will return for this screen. Undeclared fields may be dropped and unavailable for binding even if the endpoint returns them.
    ```json
    {
      "data": {
        "first_name": {
          "type": "string",
          "__example__": "John"
        }
      }
    }
    ```
*   **`refresh_on_back`** (Optional, Data Endpoint only): Defaults to `false`. Defines whether to trigger a data exchange request when using the back button to return to this screen.
    *   **`false`**: Screen loads with previously provided data/input. Avoids roundtrip; snappier experience.
    *   **`true`**: Sends a request to the Endpoint (Action: `BACK`, Screen: Name of previous screen) to revalidate/update data.
*   **`sensitive`** (Optional, v5.1+): Defaults to `[]`. Array of field names containing sensitive data. When the Flow completes, the summary message will mask these fields based on the component type. Each field name must correspond to an input component (with matching `name` attribute) that exists on the same screen. Do not include fields from other screens. Review/summary screens that only display text (no input components) should not have a `sensitive` array.

### Sensitive Fields Masking Behavior
| Component           | Masking | Consumer Experience |
| :------------------ | :------ | :------------------ |
| Text Input          | ✅       | Masked (••••)       |
| Password / OTP      | ❌       | Hidden completely   |
| Text Area           | ✅       | Masked (••••)       |
| Date Picker         | ✅       | Masked (••••)       |
| Dropdown            | ✅       | Masked (••••)       |
| Checkbox Group      | ✅       | Masked (••••)       |
| Radio Buttons Group | ✅       | Masked (••••)       |
| Opt In              | ❌       | Display as-is       |
| Document Picker     | ✅       | Hidden completely   |
| Photo Picker        | ✅       | Hidden completely   |

---

## Layout

Layout represents screen UI Content.
*   **`type`**: Currently, only `"SingleColumnLayout"` (vertical flexbox container) is available.
*   **`children`**: An array of components from the WhatsApp Flows Library.

---

## Routing Model

Required only when using an Endpoint. It is a directed graph where screens are nodes and transitions are edges.

*   **Structure**: A map of screen IDs to an array of possible next screen IDs.
*   **Limits**: Maximum 10 "branches" or connections.
*   **Entry Screen**: A screen with no inbound edge.
*   **Terminal**: All routes must eventually end at a terminal screen.

**Example:**
```json
"routing_model": {
  "MY_FIRST_SCREEN": ["MY_SECOND_SCREEN"],
  "MY_SECOND_SCREEN": ["MY_THIRD_SCREEN"]
}
```

---

## Properties (Static vs. Dynamic)

### Static Properties
Set once and never change.
```json
"title": "Demo Screen"
```

### Dynamic Properties
Set content based on server or user data.
*   **Form properties**: `${form.field_name}` (Data entered by user).
*   **Screen properties**: `${data.field_name}` (Data provided by server or previous screen).

Supported types: `string`, `number`, `boolean`, `object`, `array`.

### Nested Expressions (v6.0+)
Allows conditionals and string concatenation. Must be wrapped in backticks (`` ` ``).
*   **Equality**: `==`, `!=`
*   **Math Comparisons**: `<`, `<=`, `>`, `>=`
*   **Logical**: `&&`, `||`
*   **Math Operations**: `+`, `-`, `/`, `%` (numbers only, not for strings)
*   **Escaping**: To use backticks inside a string, use double backslash: `\\` before it.

**Examples:**
*   **String Concatenation**: ``"text": "`'Hello ' ${form.first_name}`"`` (no `+` operator, just place strings next to each other)
*   **Multiple strings**: ``"text": "`${data.street} ', ' ${data.city} ', ' ${data.country}`"``
*   **Math**: ``"text": "`'Amount: ' ${data.total} / ${form.group_size}`"``
*   **Logic**: ``"visible": "`(${form.age} > 18) && ${form.accept}`"``
*   **Equality**: ``"visible": "`${form.first_name} == ${form.last_name}`"``

**Important:** Outside backticked nested expressions, a property cannot mix static text with `${...}`. Use either a pure dynamic value (e.g., `"text": "${data.name}"`) or the backtick form above for string concatenation.

---

## Forms and Data Handling

### Forms (Legacy vs v4.0+)
*   **Before v4.0**: Inputs had to be wrapped in a `"type": "Form"` component.
*   **v4.0+**: The `Form` component is optional. You can place interactive components directly in the layout.

### Form Configuration
*   **`init-values`**: Key-value object to pre-fill inputs. Types must match the component (e.g., Array of Strings for CheckboxGroup, String for TextInput).
*   **`error-messages`**: Key-value object to set custom errors.

**Example (v4.0+ No Form Component):**
```json
{
  "id": "DEMO_SCREEN",
  "data": {
     "init_values": { "first_name": "Jon" }
  },
  "layout": {
      "type": "SingleColumnLayout",
      "children": [
         {
             "type": "TextInput",
             "name": "first_name",
             "label": "First Name",
             "init-value": "${data.init_values.first_name}"
         }
      ]
  }
}
```

### Global Dynamic Referencing (v4.0+)
Allows accessing data from any screen globally.
**Syntax:** `${screen.<screen_name>.(form|data).<field-name>}`

*   **`screen`**: Global variable keyword.
*   **`screen_name`**: ID of the screen to reference.
*   **`(form|data)`**: Storage type.
*   **`field-name`**: Specific field.

**Use Case:** You no longer need to pass payloads explicitly in the `navigate` action to carry data forward.
**Practical tip:** For reliability, especially across different Flow JSON versions, also include needed values in the `data` you return for the next screen (e.g., echo `user_email` and `selected_date` forward) so components can bind via `${data.*}` even if global references are unavailable.

---

## Actions

Actions trigger asynchronous logic.

### 1. `navigate`
Transitions to the next screen.
*   **Payload**: Required, even if empty `{}`. Data passed here is available in the next screen via `${data.field}`.
*   **Note**: Do not use on the footer of a terminal screen.

```json
"on-click-action": {
  "name": "navigate",
  "next": { "type": "screen", "name": "NEXT_SCREEN" },
  "payload": {
    "name": "${form.first_name}"
  }
}
```

With no data to pass:
```json
"on-click-action": {
  "name": "navigate",
  "next": { "type": "screen", "name": "NEXT_SCREEN" },
  "payload": {}
}
```

### 2. `complete`
Terminates the flow and sends the payload to the chat thread via webhook.
*   **Recommendation**: Keep payload size minimum. Avoid base64 images.

```json
"on-click-action": {
  "name": "complete",
  "payload": {
    "selected_items": "${form.selected_items}"
  }
}
```

### 3. `data_exchange`
Sends data to the WhatsApp Flows Data Endpoint. The server response determines the next step.
*   Payload keys are forwarded as-is to your endpoint. Avoid generic names like `action` that may collide with Meta fields; prefer explicit keys such as `user_action`, `step`, `intent`.

```json
"on-click-action": {
  "name": "data_exchange",
  "payload": {
    "discount_code": "${data.discount_code}"
  }
}
```

### 4. `update_data` (v6.0+)
Triggers an immediate update to the screen's state based on user interaction (e.g., changing a dropdown list based on a previous selection) without a full screen refresh.

```json
"on-click-action": {
  "name": "update_data",
  "payload": {
    "state_list": "${data.countries[form.selected_country].states}"
  }
}
```

### 5. `open_url` (v6.0+)
Opens a URL in the device's default browser.
*   **Supported Components**: `EmbeddedLink` and `OptIn`.
*   **Payload**: Accepts only a `url` property.

```json
{
   "type": "EmbeddedLink",
   "text": "Terms and Conditions",
   "on-click-action": {
      "name": "open_url",
      "url": "https://www.whatsapp.com/"
   }
}
```

---

## Limitations

*   **File Size**: Flow JSON content string cannot exceed **10 MB**.
*   


# Flow JSON Components

Components are the building blocks of WhatsApp Flows. They allow you to build complex UIs and display business data using attribute models.

*   **Limit:** The maximum number of components per screen is **50**.

**Supported Components:**
*   **Text:** Heading, Subheading, Caption, Body, RichText
*   **Input:** TextEntry (TextInput, TextArea), DatePicker, CalendarPicker, Media upload
*   **Selection:** CheckboxGroup, RadioButtonsGroup, Dropdown, Chips Selector, Switch
*   **Display/Media:** Image, Image Carousel
*   **Navigation/Action:** Footer, OptIn, EmbeddedLink, NavigationList
*   **Logic:** If

---

## Text Components

### Heading
Top level title of a page.
| Parameter | Description                                 |
| :-------- | :------------------------------------------ |
| `type`    | (Required) `"TextHeading"`                  |
| `text`    | (Required) String or Dynamic `${data.text}` |
| `visible` | Boolean. Default: `true`.                   |

### Subheading
| Parameter | Description                                 |
| :-------- | :------------------------------------------ |
| `type`    | (Required) `"TextSubheading"`               |
| `text`    | (Required) String or Dynamic `${data.text}` |
| `visible` | Boolean. Default: `true`.                   |

### Body
| Parameter       | Description                                           |
| :-------------- | :---------------------------------------------------- |
| `type`          | (Required) `"TextBody"`                               |
| `text`          | (Required) String or Dynamic `${data.text}`           |
| `font-weight`   | Enum: `{'bold','italic','bold_italic','normal'}`      |
| `strikethrough` | Boolean                                               |
| `visible`       | Boolean. Default: `true`.                             |
| `markdown`      | Boolean. Default: `false`. (Requires Flow JSON V5.1+) |

### Caption
| Parameter       | Description                                           |
| :-------------- | :---------------------------------------------------- |
| `type`          | (Required) `"TextCaption"`                            |
| `text`          | (Required) String or Dynamic `${data.text}`           |
| `font-weight`   | Enum: `{'bold','italic','bold_italic','normal'}`      |
| `strikethrough` | Boolean                                               |
| `visible`       | Boolean. Default: `true`.                             |
| `markdown`      | Boolean. Default: `false`. (Requires Flow JSON V5.1+) |

### Markdown Support (v5.1+)
`TextBody` and `TextCaption` support limited markdown if `markdown: true` is set.
```json
{
   "type": "TextBody",
   "markdown": true,
   "text": [ "This text is ~~***really important***~~" ]
}
```

### Limits and Restrictions
| Component  | Property        | Limit / Restriction                  |
| :--------- | :-------------- | :----------------------------------- |
| Heading    | Character Limit | 80                                   |
| Subheading | Character Limit | 80                                   |
| Body       | Character Limit | 4096                                 |
| Caption    | Character Limit | 409                                  |
| All        | Text            | Empty or Blank value is not accepted |

---

## RichText (v5.1+)
Provides rich formatting capabilities and rendering for large texts (Terms, Policies, etc.).

| Parameter | Description                                               |
| :-------- | :-------------------------------------------------------- |
| `type`    | (Required) `"RichText"`                                   |
| `text`    | (Required) String or String Array. Dynamic `${data.text}` |
| `visible` | Boolean. Default: `true`.                                 |

**Note:**
*   **Until V6.2:** Must be a standalone component (cannot share screen with others).
*   **Starting V6.3:** Can be used with a `Footer` component.

### Supported Syntax
*   **Headings:** `# Heading 1`, `## Heading 2` (Others render as body text).
*   **Formatting:** `**bold**`, `*italic*`, `~~strikethrough~~`.
*   **Lists:** Ordered (`1. Item`) and Unordered (`- Item` or `+ Item`).
*   **Images:** Base64 inline only. `![Alt](data:image/png;base64,...)`.
    *   *Formats:* png, jpg/jpeg, webp (iOS 14.6+).
*   **Links:** `[Text](URL)`
*   **Tables:** Standard Markdown syntax. Columns widths based on header content size.

### Syntax Cheatsheet
| Syntax                               | RichText | TextBody | TextCaption |
| :----------------------------------- | :------- | :------- | :---------- |
| `# H1`, `## H2`                      | ✅        | ❌        | ❌           |
| `**bold**`, `*italic*`, `~~strike~~` | ✅        | ✅        | ✅           |
| Lists (`+`, `-`, `1.`)               | ✅        | ✅        | ✅           |
| `[Link](url)`                        | ✅        | ✅        | ✅           |
| `![Image](base64)`                   | ✅        | ❌        | ❌           |
| Tables                               | ✅        | ❌        | ❌           |

---

## Text Entry Components

### TextInput
| Parameter       | Description                                                        |
| :-------------- | :----------------------------------------------------------------- |
| `type`          | (Required) `"TextInput"`                                           |
| `name`          | (Required) String                                                  |
| `label`         | (Required) String.                                                 |
| `label-variant` | `large` (v7.0+) - Prominent style, multi-line support.             |
| `input-type`    | Enum: `{'text','number','email', 'password', 'passcode', 'phone'}` |
| `pattern`       | (v6.2+) Regex string. Requires `helper-text`.                      |
| `required`      | Boolean                                                            |
| `min-chars`     | Integer                                                            |
| `max-chars`     | Integer. Default: 80.                                              |
| `helper-text`   | String                                                             |
| `visible`       | Boolean. Default: `true`.                                          |
| `init-value`    | (v4.0+) String. Only outside `Form`.                               |
| `error-message` | (v4.0+) String. Only outside `Form`.                               |

### TextArea
| Parameter       | Description                          |
| :-------------- | :----------------------------------- |
| `type`          | (Required) `"TextArea"`              |
| `name`          | (Required) String                    |
| `label`         | (Required) String.                   |
| `label-variant` | `large` (v7.0+)                      |
| `max-length`    | Integer. Default: 600.               |
| `helper-text`   | String                               |
| `enabled`       | Boolean                              |
| `visible`       | Boolean. Default: `true`.            |
| `init-value`    | (v4.0+) String. Only outside `Form`. |
| `error-message` | (v4.0+) String. Only outside `Form`. |

### Limits
| Component | Property           | Limit              |
| :-------- | :----------------- | :----------------- |
| TextInput | Helper/Error/Label | 80 / 30 / 20 chars |
| TextArea  | Helper/Label       | 80 / 20 chars      |

---

## Selection Components

### CheckboxGroup & RadioButtonsGroup
**CheckboxGroup:** Pick multiple. **RadioButtonsGroup:** Pick one.

| Parameter            | Description                                         |
| :------------------- | :-------------------------------------------------- |
| `type`               | `"CheckboxGroup"` or `"RadioButtonsGroup"`          |
| `name`               | (Required) String                                   |
| `data-source`        | (Required) Array of objects (See structure below)   |
| `label`              | String (Required v4.0+)                             |
| `min-selected-items` | Integer (Checkbox only)                             |
| `max-selected-items` | Integer (Checkbox only)                             |
| `enabled`            | Boolean                                             |
| `required`           | Boolean                                             |
| `visible`            | Boolean                                             |
| `on-select-action`   | Action (`data_exchange`, `update_data` v6.0+)       |
| `on-unselect-action` | (v6.0+) Action (`update_data` only)                 |
| `init-value`         | Array<String> (Checkbox) or String (Radio). (v4.0+) |
| `media-size`         | (v5.0+) Enum: `{'regular', 'large'}`                |

**Data Source Structure:**
*   **< v5.0:** `id`, `title`, `description`, `metadata`, `enabled`.
*   **> v5.0:** Adds `image` (Base64), `alt-text`, `color` (hex).
*   **> v6.0:** Adds `on-select-action`, `on-unselect-action`.

**Limits:**
*   **Items:** Min 1, Max 20.
*   **Image Size:** 100KB (v6.0+), 300KB (<v6.0).
*   **Text:** Title (30), Desc (30), Meta (300).

### Dropdown
| Parameter            | Description                                         |
| :------------------- | :-------------------------------------------------- |
| `type`               | (Required) `"Dropdown"`                             |
| `label`              | (Required) String                                   |
| `data-source`        | (Required) Array (Same structure as Checkbox/Radio) |
| `required`           | Boolean                                             |
| `enabled`            | Boolean                                             |
| `visible`            | Boolean                                             |
| `on-select-action`   | Action (`data_exchange`, `update_data`)             |
| `on-unselect-action` | (v6.0+) Action (`update_data` only)                 |
| `init-value`         | String (v4.0+)                                      |

**Limits:**
*   **Items:** Max 200 (no images), Max 100 (with images).
*   **Text:** Label (20), Title (30), Desc (300).

### Chips Selector (v6.3+)
Allows picking multiple selections.

| Parameter                | Description                                                               |
| :----------------------- | :------------------------------------------------------------------------ |
| `type`                   | `"ChipsSelector"`                                                         |
| `name`                   | (Required) String                                                         |
| `data-source`            | Array: `id`, `title`, `enabled`, `on-select-action`, `on-unselect-action` |
| `label`                  | (Required) String                                                         |
| `min/max-selected-items` | Integer                                                                   |
| `on-select-action`       | Action (`data_exchange`, `update_data` v7.1+)                             |
| `on-unselect-action`     | (v7.1+) Action (`update_data` only)                                       |

**Limits:** Min 2 options, Max 20 options. Label 80 chars.

### Switch (v4.0+)
Renders components based on a value match.

| Parameter | Description                                                      |
| :-------- | :--------------------------------------------------------------- |
| `type`    | `"Switch"`                                                       |
| `value`   | (Required) String variable to evaluate (e.g., `${data.animal}`)  |
| `cases`   | (Required) Map. Key = string value. Value = Array of Components. |

---

## Action & Navigation Components

### Footer
| Parameter         | Description       |
| :---------------- | :---------------- |
| `type`            | `"Footer"`        |
| `label`           | (Required) String |
| `on-click-action` | (Required) Action |
| `left-caption`    | String            |
| `center-caption`  | String            |
| `right-caption`   | String            |

*   **Rule:** Can set Left+Right OR Center, but not all 3.
*   **Limits:** Label (35 chars), Captions (15 chars). 1 Footer per screen.

### OptIn
| Parameter                   | Description                                                 |
| :-------------------------- | :---------------------------------------------------------- |
| `type`                      | `"OptIn"`                                                   |
| `label`                     | (Required) String                                           |
| `name`                      | (Required) String                                           |
| `on-click-action`           | Action (Triggers "Read more"). Supports `open_url` (v6.0+). |
| `on-select/unselect-action` | (v6.0+) `update_data` only.                                 |

*   **Limits:** 120 chars, Max 5 per screen.

### EmbeddedLink
| Parameter         | Description                                                       |
| :---------------- | :---------------------------------------------------------------- |
| `type`            | `"EmbeddedLink"`                                                  |
| `text`            | (Required) String                                                 |
| `on-click-action` | (Required) Action (`navigate`, `data_exchange`, `open_url` v6.0+) |

*   **Limits:** 25 chars, Max 2 per screen.

### NavigationList (v6.2+)
List of options to navigate between screens.

| Parameter         | Description                                                       |
| :---------------- | :---------------------------------------------------------------- |
| `type`            | `"NavigationList"`                                                |
| `list-items`      | (Required) Array.                                                 |
| `on-click-action` | `data_exchange` or `navigate`. Can be on component or item level. |

**Item Structure:**
*   `main-content` (Req): `title` (30 chars), `description` (20), `metadata` (80).
*   `start`: `image` (Base64, 100KB), `alt-text`.
*   `end`: `title` (10 chars), `description` (10), `metadata` (10).
*   `badge`: String (15 chars). Max 1 item with badge per list.
*   `tags`: Array<string> (Max 3).

**Restrictions:** Max 2 per screen. Cannot be on terminal screen. Cannot be combined with other components.

---

## Date & Time Components

### DatePicker
**Important (v5.0+):** Uses formatted date string `"YYYY-MM-DD"`. This decouples values from time zones.

| Parameter               | Description                                       |
| :---------------------- | :------------------------------------------------ |
| `type`                  | `"DatePicker"`                                    |
| `name`                  | (Required) String                                 |
| `label`                 | (Required) String                                 |
| `min-date` / `max-date` | String (Timestamp in ms). See *Guidelines* below. |
| `unavailable-dates`     | Array <Timestamp in ms>.                          |
| `on-select-action`      | Only `data_exchange`.                             |

**Guidelines (< v5.0):**
If business and user are in different time zones, timezone offsets can cause incorrect dates.
*   *Recommendation:* Use v5.0+ logic ("YYYY-MM-DD") or calculate offsets carefully using the user's timezone (collected via Dropdown).

### CalendarPicker (v6.1+)
Full calendar interface.

| Parameter             | Description                                                  |
| :-------------------- | :----------------------------------------------------------- |
| `type`                | `"CalendarPicker"`                                           |
| `mode`                | `"single"` (Default) or `"range"`.                           |
| `label`               | String. If range: `{"start-date": "...", "end-date": "..."}` |
| `min-date`/`max-date` | String `"YYYY-MM-DD"`.                                       |
| `include-days`        | Array<Enum> (Mon, Tue, etc.). Default: All.                  |
| `min-days`/`max-days` | Integer (Range mode only).                                   |
| `on-select-action`    | `data_exchange`. Payload: String (Single) or Object (Range). |

---

## Media Components

### Image
| Parameter          | Description                     |
| :----------------- | :------------------------------ |
| `type`             | `"Image"`                       |
| `src`              | (Required) Base64 string.       |
| `scale-type`       | `cover` or `contain` (Default). |
| `aspect-ratio`     | Number. Default 1.              |
| `width` / `height` | Integer.                        |

*   **Limits:** Max 3 per screen, 300kb (Rec), 1MB Payload limit. Format: JPEG, PNG.

### Image Carousel (v7.1+)
Slide through multiple images.

| Parameter      | Description                           |
| :------------- | :------------------------------------ |
| `type`         | `"ImageCarousel"`                     |
| `images`       | (Required) Array `{ src, alt-text }`. |
| `aspect-ratio` | `"4:3"` (Default) or `"16:9"`.        |
| `scale-type`   | `cover` or `contain` (Default).       |

*   **Limits:** Min 1, Max 3 images. Max 2 carousels per screen.

### PhotoPicker (v4.0+)
Allows users to upload photos from camera or gallery.

| Parameter              | Description                                                                 |
| :--------------------- | :-------------------------------------------------------------------------- |
| `type`                 | (Required) `"PhotoPicker"`                                                  |
| `name`                 | (Required) String. Must be unique on the screen.                            |
| `label`                | (Required) String. Max 80 chars. Dynamic supported.                         |
| `description`          | String. Max 300 chars. Dynamic supported.                                   |
| `photo-source`         | Enum: `"camera_gallery"` (default), `"camera"`, `"gallery"`                 |
| `max-file-size-kb`     | Integer (kibibytes). Default: 25600 (25 MiB). Range: [1, 25600]             |
| `min-uploaded-photos`  | Integer. Default: 0. Range: [0, 30]. Use this instead of `required`.        |
| `max-uploaded-photos`  | Integer. Default: 30. Range: [1, 30]                                        |
| `enabled`              | Boolean. Default: true                                                      |
| `visible`              | Boolean. Default: true                                                      |
| `error-message`        | String or Object for image-specific errors                                  |

**Important:**
- `required` property is NOT supported. Use `min-uploaded-photos: 1` for required.
- Only 1 PhotoPicker allowed per screen.
- Cannot use PhotoPicker and DocumentPicker on the same screen.
- Cannot be used in `navigate` action payload. Use `data_exchange` or `complete`.
- Must be top-level in action payload: `"media": "${form.photo_picker}"` (not nested).

### DocumentPicker (v4.0+)
Allows users to upload documents.

| Parameter                 | Description                                                              |
| :------------------------ | :----------------------------------------------------------------------- |
| `type`                    | (Required) `"DocumentPicker"`                                            |
| `name`                    | (Required) String. Must be unique on the screen.                         |
| `label`                   | (Required) String. Max 80 chars. Dynamic supported.                      |
| `description`             | String. Max 300 chars. Dynamic supported.                                |
| `max-file-size-kb`        | Integer (kibibytes). Default: 25600 (25 MiB). Range: [1, 25600]          |
| `min-uploaded-documents`  | Integer. Default: 0. Range: [0, 30]. Use this instead of `required`.     |
| `max-uploaded-documents`  | Integer. Default: 30. Range: [1, 30]                                     |
| `allowed-mime-types`      | Array of strings. See supported list below.                              |
| `enabled`                 | Boolean. Default: true                                                   |
| `visible`                 | Boolean. Default: true                                                   |
| `error-message`           | String or Object for document-specific errors                            |

**Supported MIME types:** `application/pdf`, `application/msword`, `application/vnd.openxmlformats-officedocument.wordprocessingml.document`, `application/vnd.ms-excel`, `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`, `application/vnd.ms-powerpoint`, `application/vnd.openxmlformats-officedocument.presentationml.presentation`, `application/zip`, `application/gzip`, `application/x-7z-compressed`, `text/plain`, `image/jpeg`, `image/png`, `image/gif`, `image/webp`, `image/heic`, `image/heif`, `image/avif`, `image/tiff`, `video/mp4`, `video/mpeg`

**Important:**
- `required` property is NOT supported. Use `min-uploaded-documents: 1` for required.
- Only 1 DocumentPicker allowed per screen.
- Cannot use PhotoPicker and DocumentPicker on the same screen.
- Cannot be used in `navigate` action payload. Use `data_exchange` or `complete`.
- Must be top-level in action payload: `"media": "${form.document_picker}"` (not nested).

### Media Upload Limits
- Max 10 files can be sent in response message.
- Max aggregated size: 100 MiB for response message attachments.
- Files stored in WhatsApp CDN for up to 20 days (encrypted).

---

## Logic Components

### If (v4.0+)
Conditional rendering.

| Parameter   | Description                           |
| :---------- | :------------------------------------ |
| `type`      | `"If"`                                |
| `condition` | (Required) Boolean expression string. |
| `then`      | (Required) Array of Components.       |
| `else`      | Array of Components.                  |

**Operators:** `==`, `!=`, `&&`, `||`, `!`, `>`, `>=`, `<`, `<=`, `()`.
**Rules:**
*   Condition must resolve to boolean.
*   Max nesting: 3 levels.
*   **Footer Rule:** If used inside `If`, it must exist in *both* `then` and `else` branches (or neither), and no footer can exist outside the `If`.

---

## Dynamic Components (Tutorial)

This pattern allows updating the UI (e.g., refreshing time slots) when a user interacts with a component (e.g., selects a date).

**Prerequisites:** Requires a Data Endpoint and `data_api_version: "3.0"`.

### Step 1: Define the Screen
Create a screen with a `DatePicker` and a `Dropdown`.

```json
{
  "version": "7.2",
  "data_api_version": "3.0",
  "routing_model": { "BOOKING": [] },
  "screens": [
    {
      "id": "BOOKING",
      "terminal": true,
      "title": "Booking appointment",
      "data": {
        "is_dropdown_visible": { "type": "boolean", "__example__": false },
        "available_slots": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": { "id": {"type": "string"}, "title": {"type": "string"} }
            },
            "__example__": []
        }
      },
      "layout": {
        "type": "SingleColumnLayout",
        "children": [
          {
            "type": "DatePicker",
            "name": "date",
            "label": "Select date",
            "on-select-action": {
                "name": "data_exchange",
                "payload": {
                    "date": "${form.date}",
                    "component_action": "update_date"
                }
            }
          },
          {
             "type": "Dropdown",
             "label": "Pick a slot",
             "name": "slot",
             "required": "${data.is_dropdown_visible}",
             "visible": "${data.is_dropdown_visible}",
             "data-source": "${data.available_slots}"
          },
          {
             "type": "Footer",
             "label": "Book",
             "on-click-action": { "name": "complete", "payload": {} }
          }
        ]
      }
    }
  ]
}
```

### Step 2: Server Response
When the user selects a date, the `on-select-action` triggers `data_exchange`. Your server should respond with the new data to update the screen state.

**Expected Server Response Payload:**
```json
{
  "version": "3.0",
  "screen": "BOOKING",
  "data": {
    "is_dropdown_visible": true,
    "available_slots": [
      { "id": "1", "title": "08:00" },
      { "id": "2", "title": "09:00" }
    ]
  }
}
```

---

## Common Patterns & Gotchas

### Required + Visible for Inputs

Many input and selection components (`TextInput`, `Dropdown`, `CheckboxGroup`,
`RadioButtonsGroup`, etc.) support both `required` and `visible`.

- **Important:** WhatsApp Flows validation will fail if a component is
  effectively hidden but marked as required (for example,
  `required: true` and `visible: false`).
- When you hide a component until data is ready, either:
  - keep `required` omitted or `false` while it is hidden and only validate on a
    later screen, or
  - bind `required` to the same condition as visibility, for example:
    `required: "${data.is_dropdown_visible}"`.

The **Dynamic Components (Tutorial)** above uses this pattern for the `Dropdown`
that is only required once it is visible.

### Dynamic dropdowns (invisible → visible)

For patterns where:

- The user first picks a date or filter.
- The endpoint returns a set of options.
- A dropdown becomes visible once options are available.

Recommended pattern:

1. In `data`, declare:
   - a boolean flag like `is_dropdown_visible` (default `false`),
   - an array like `available_slots` with the `{ id, title }` structure.
2. In the layout:
   - bind `visible` to `${data.is_dropdown_visible}`,
   - bind `data-source` to `${data.available_slots}`,
   - optionally bind `required` to the same visibility flag.
3. In the endpoint response:
   - set `is_dropdown_visible: true` when you have options,
   - populate `available_slots` with the list of choices.

This keeps validation rules consistent and avoids hidden-but-required inputs.

### Routing by action + screen

For data endpoints, prefer routing using:

- `data_exchange.action`: `"INIT"`, `"data_exchange"`, `"BACK"`,
- `data_exchange.screen`: the current screen ID.

Custom fields inside `data` (for example, `data.action`) can still be used as
secondary hints, but they should not replace the canonical
`action + screen` decision tree described in the **Data Endpoint Quick Start**.

---

# Additional Technical Reference

## Logic Component Details (`If`)

### Supported Operators Reference
The `condition` property supports specific operators and data types.

| Operator          | Symbol | Types Allowed           | Rules & Return Type                                                                                      |
| :---------------- | :----- | :---------------------- | :------------------------------------------------------------------------------------------------------- |
| **Parentheses**   | `()`   | Boolean, Number, String | Used to define precedence. <br> *Example:* `${form.opt} \|\| (${data.num} > 5)`                          |
| **Equal**         | `==`   | Boolean, Number, String | Both sides must be the same type. <br> *Example:* `${form.city} == 'London'`                             |
| **Not Equal**     | `!=`   | Boolean, Number, String | Both sides must be the same type. <br> *Example:* `${data.val} != 5`                                     |
| **AND**           | `&&`   | Boolean                 | Evaluates true only if both sides are true. High priority. <br> *Example:* `${form.opt} && ${data.bool}` |
| **OR**            | `\|\|` | Boolean                 | Evaluates true if at least one side is true. <br> *Example:* `${form.opt} \|\| ${data.bool}`             |
| **NOT**           | `!`    | Boolean                 | Negates the statement. <br> *Example:* `!(${data.num} > 5)`                                              |
| **Greater Than**  | `>`    | Number                  | *Example:* `${data.num} > 5`                                                                             |
| **Greater/Equal** | `>=`   | Number                  | *Example:* `${data.num} >= 5`                                                                            |
| **Less Than**     | `<`    | Number                  | *Example:* `${data.num} < 5`                                                                             |
| **Less/Equal**    | `<=`   | Number                  | *Example:* `${data.num} <= 5`                                                                            |

### Validation Errors & Limitations (`If`)
These are the specific validation errors you will encounter during Flow compilation if rules are violated.

| Scenario                                        | Validation Error Message                                                                           |
| :---------------------------------------------- | :------------------------------------------------------------------------------------------------- |
| Footer exists in `then`, but `else` is missing. | *Missing Footer inside one of the if branches. Branch "else" should exist and contain one Footer.* |
| Footer exists in `then`, but not in `else`.     | *Missing Footer inside one of the if branches.*                                                    |
| Footer exists in `else`, but not in `then`.     | *Missing Footer inside one of the if branches.*                                                    |
| Footer exists inside `If` AND outside `If`.     | *You can only have 1 Footer component per screen.*                                                 |
| The `then` array is empty.                      | *Invalid value found at: ".../then" due to empty array. It should contain at least one component.* |

---

## Logic Component Details (`Switch`)

### Validation Errors (`Switch`)

| Scenario                       | Validation Error Message                        |
| :----------------------------- | :---------------------------------------------- |
| The `cases` property is empty. | *Invalid empty property found at: ".../cases".* |

---

## Rich Text Best Practices

### Working with Large Texts
While `RichText` allows static text arrays, it is recommended to use **dynamic data** for large documents (like Terms of Service).
*   **Why?** Improves JSON readability and allows you to update the text from your server without changing the Flow JSON.
*   **How?** Send the markdown string as a normal string property in your data payload (no need to convert it to an array of strings).

**Example:**
```json
{
   "type": "RichText",
   "text": "${data.terms_of_service_content}"
}
```

---

## Navigation List: Specific Restrictions

While the general limits were provided, note these specific behaviors for `NavigationList`:

1.  **Image Placeholder:** If an image in the `start` object exceeds **100KB**, it will not be displayed; it will be replaced by a generic placeholder.
2.  **Truncation:**
    *   `label` content (>80 chars) will truncate.
    *   `description` content (>300 chars) will truncate.
    *   `list-items` content (>20 items) will not render if the limit is reached.
3.  **Action definition:** You cannot define `on-click-action` on **both** the component level and the item level simultaneously. It must be one or the other.

---

# UX & Content Best Practices

These guidelines are distilled from Meta’s Flows docs and are intended to
produce flows that feel fast, clear, and trustworthy.

## Flow Length & Screen Design

- **Keep flows short.** Design flows so a typical user can complete the main
  task in **under ~5 minutes**.
- **One primary task per screen.** Avoid screens that try to collect many
  unrelated inputs at once. If you need multiple tasks, split them into
  separate screens.
- **Limit components per screen.**
  - Too many components make the layout noisy and slow to load.
  - Consider what happens if a user leaves mid-flow: fewer components per
    screen means less data lost when they re-enter.
- **Use the right screen for the right task.**
  - If a sub-flow is needed (for example, “Forgot password”), keep it
    **small (≤ 3 screens)** and return the user to the main task afterwards.

## Initiation & Navigation

- **Initiation message should match the first screen.**
  - The chat message + CTA that opens the flow must clearly describe the task.
  - The first screen should immediately reflect that task—no surprises.
- **Set expectations up front.**
  - In either the chat copy or the first screen, indicate roughly how long it
    will take (for example, “This will take about 2–3 minutes.”).
- **Use clear, action-oriented titles.**
  - Examples: “Book appointment”, “Confirm details”, “Update contact info”.
  - Use titles to show progress where it helps, for example “Step 1 of 3”.
- **Always end with a summary / confirmation screen.**
  - Show what the user is about to submit (or what was submitted).
  - Make the final CTA explicit, for example “Confirm booking”.

## CTAs, Copy & Style

- **CTAs should describe the outcome.**
  - Good: “Confirm booking”, “Submit application”.
  - Weak: “Continue”, “Next”.
- **Use sentence case consistently.**
  - Prefer “Book appointment” over “BOOK APPOINTMENT”.
- **Use emojis sparingly.**
  - Only when they add clarity or match brand tone.
  - Avoid them in critical or highly formal flows.
- **Avoid redundant copy.**
  - Don’t repeat the same phrase in title and body without adding information
    (for example, avoid “Complete registration” and then “Complete registration
    below”).

## Forms & Input Quality

- **Choose the appropriate component:**
  - Use `DatePicker` / `CalendarPicker` for dates (not free-text).
  - Use `TextArea` for long free-text answers.
  - Use `Dropdown` / `RadioButtonsGroup` / `CheckboxGroup` instead of free text
    when the set of options is known.
- **Order fields logically.**
  - For example: first name → last name → email → phone.
- **Make non-critical fields optional.**
  - Only mark fields as required if they are truly needed to complete the task.
- **Use helper text for tricky fields.**
  - Example: phone number formats, password rules, date formats.

## Options & Lists

- **Keep option lists small.**
  - Aim for **≤ 10 options per screen** when possible.
- **Pick the right selection component:**
  - Use **RadioButtons** when the user must pick exactly one option.
  - Use **CheckboxGroup** when multiple selections are allowed.
  - Use **Dropdown** when there are many options (Meta recommends using it
    when there are ~8 or more choices).
- **Use sensible defaults.**
  - Where appropriate, make the first option (or the most common one) the
    default selection.

## Error Handling & Validation UX

- **Errors should say what happened and how to fix it.**
  - For example: “This email looks invalid. Please check the format.” instead
    of just “Error”.
- **Show validation rules up front.**
  - Use helper text for passwords, date formats, numeric ranges, etc.
- **Prefer staying in-flow on errors.**
  - When endpoint data becomes invalid (for example, a slot becomes
    unavailable), send the user back to the previous relevant screen with a
    clear error message rather than ending the flow abruptly.

## Login & Trust

- **Use login screens only when necessary.**
  - Logging in can feel heavy inside a flow; only require it when the task
    truly needs authentication.
- **Place login later in the flow.**
  - Show value first (for example, show summary or available options), then
    prompt for login as one of the last steps before completion.
- **Maintain sense of place.**
  - Make it clear that login is still part of the same flow, not an external
    redirect.
- **Make support easy to reach.**
  - Provide a way (inside the flow or in follow-up messages) for users to
    contact support if something goes wrong.

## Termination & Follow-up

- **Clearly describe what happens after completion.**
  - The last screen should confirm the action and set expectations for what
    happens next (for example, “We’ll send a confirmation email shortly.”).
- **Keep completion payloads lean.**
  - Only send data that is needed downstream; avoid huge payloads or embedded
    images in completion responses.
- **Bookend with a chat message.**
  - After the Flow completes, send a human-readable message to the chat that
    summarizes the outcome and provides next steps or support information.
  - Combine this with the built-in summary behavior and `sensitive` fields when
    appropriate.

## Writing & Formatting

- **Use a clear content hierarchy.**
  - Use headings for the main point, body text for explanation, and captions
    for small hints or secondary information.
- **Format data appropriately.**
  - Use correct currency symbols, phone number formats, and localized date/time
    formats that match the user’s expectations.
- **Check grammar and spelling.**
  - Read flows end-to-end before publishing; keep terminology and
    capitalization consistent.

```

### scripts/test.js

```javascript
const { hasHelpFlag, parseFlags, requireFlag } = require('./lib/webhooks/args');
const { kapsoConfigFromEnv, kapsoRequest } = require('./lib/webhooks/kapso-api');

function ok(data) {
  return { ok: true, data };
}

function err(message, details) {
  return { ok: false, error: { message, details } };
}

async function main() {
  const argv = process.argv.slice(2);
  if (hasHelpFlag(argv)) {
    console.log(
      JSON.stringify(
        {
          ok: true,
          usage: 'node scripts/test.js --webhook-id <id> [--event-type <value>]',
          env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
        },
        null,
        2
      )
    );
    return 0;
  }

  try {
    const flags = parseFlags(argv);
    const webhookId = requireFlag(flags, 'webhook-id');
    const eventType = flags['event-type'];

    const params = new URLSearchParams();
    if (eventType && eventType !== true) {
      params.set('event_type', eventType);
    }

    const config = kapsoConfigFromEnv();
    const data = await kapsoRequest(
      config,
      `/platform/v1/whatsapp/webhooks/${encodeURIComponent(webhookId)}/test${params.toString() ? `?${params.toString()}` : ''}`,
      { method: 'POST' }
    );

    console.log(JSON.stringify(ok(data), null, 2));
    return 0;
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    console.error(JSON.stringify(err('Command failed', { message }), null, 2));
    return 1;
  }
}

main().then((code) => process.exit(code));

```

### scripts/create.js

```javascript
const { kapsoConfigFromEnv, kapsoRequest } = require('./lib/webhooks/kapso-api');
const { hasHelpFlag, parseFlags, requireFlag } = require('./lib/webhooks/args');
const { buildWebhookPayload } = require('./lib/webhooks/webhook');

function ok(data) {
  return { ok: true, data };
}

function err(message, details) {
  return { ok: false, error: { message, details } };
}

function resolveScope(flags) {
  const raw = flags.scope;
  if (!raw || raw === true) {
    return 'config';
  }
  const scope = String(raw);
  if (scope !== 'config' && scope !== 'project') {
    throw new Error(`Invalid --scope value: ${scope}`);
  }
  return scope;
}

async function main() {
  const argv = process.argv.slice(2);
  if (hasHelpFlag(argv)) {
    console.log(
      JSON.stringify(
        {
          ok: true,
          usage:
            'node scripts/create.js --url <https://...> --events <csv|json-array> [--phone-number-id <id>] [--scope config|project] [--kind <kapso|meta>] [--payload-version v1|v2] [--buffer-enabled true|false] [--buffer-window-seconds <n>] [--max-buffer-size <n>] [--inactivity-minutes <n>] [--headers <json>] [--active true|false]',
          env: ['KAPSO_API_BASE_URL', 'KAPSO_API_KEY']
        },
        null,
        2
      )
    );
    return 0;
  }

  try {
    const flags = parseFlags(argv);
    const scope = resolveScope(flags);
    const payload = buildWebhookPayload(flags);
    if (!payload.url) {
      throw new Error('Missing required flag --url');
    }
    if (!payload.events) {
      throw new Error('Missing required flag --events');
    }

    const config = kapsoConfigFromEnv();
    let path = '';
    const body = { whatsapp_webhook: payload };

    if (scope === 'project') {
      const phoneNumberId = flags['phone-number-id'];
      if (phoneNumberId && phoneNumberId !== true) {
        body.whatsapp_webhook.phone_number_id = phoneNumberId;
      }
      path = '/platform/v1/whatsapp/webhooks';
    } else {
      const phoneNumberId = requireFlag(flags, 'phone-number-id');
      path = `/platform/v1/whatsapp/phone_numbers/${encodeURIComponent(phoneNumberId)}/webhooks`;
    }

    const data = await kapsoRequest(config, path, {
      method: 'POST',
      body: JSON.stringify(body)
    });

    console.log(JSON.stringify(ok(data), null, 2));
    return 0;
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    console.error(JSON.stringify(err('Command failed', { message }), null, 2));
    return 1;
  }
}

main().then((code) => process.exit(code));

```

### assets/template-utility-order-status-update.json

```json
{
  "name": "order_status_update",
  "language": "en_US",
  "category": "UTILITY",
  "parameter_format": "NAMED",
  "components": [
    {
      "type": "HEADER",
      "format": "TEXT",
      "text": "Order {{order_id}} update",
      "example": {
        "header_text_named_params": [
          { "param_name": "order_id", "example": "ORDER-123" }
        ]
      }
    },
    {
      "type": "BODY",
      "text": "Hi {{customer_name}}, your order is {{status}}. {{details}}",
      "example": {
        "body_text_named_params": [
          { "param_name": "customer_name", "example": "Alex" },
          { "param_name": "status", "example": "shipped" },
          { "param_name": "details", "example": "Expected delivery: Jan 25" }
        ]
      }
    },
    {
      "type": "FOOTER",
      "text": "Reply STOP to opt out"
    },
    {
      "type": "BUTTONS",
      "buttons": [
        {
          "type": "URL",
          "text": "Track order",
          "url": "https://example.com/orders/{{1}}",
          "example": ["https://example.com/orders/ORDER-123"]
        }
      ]
    }
  ]
}


```

### scripts/create-flow.js

```javascript
#!/usr/bin/env node
const { parseArgs, getStringFlag, getBooleanFlag, readFlagJson } = require('./lib/cli');
const { platformRequest } = require('./lib/http');
const { run } = require('./lib/run');

run(async () => {
  const { flags } = parseArgs(process.argv.slice(2));
  const phoneNumberId = getStringFlag(flags, 'phone-number-id') || getStringFlag(flags, 'phone_number_id');
  if (!phoneNumberId) {
    throw new Error('Missing required flag --phone-number-id');
  }

  const name = getStringFlag(flags, 'name');
  const publish = getBooleanFlag(flags, 'publish');
  const flowJson = await readFlagJson(flags, 'flow-json', 'flow-json-file');

  const body = {
    phone_number_id: phoneNumberId
  };

  if (name) body.name = name;
  if (flowJson) body.flow_json = flowJson;
  if (publish) body.publish = true;

  return platformRequest({
    method: 'POST',
    path: '/platform/v1/whatsapp/flows',
    body
  });
});

```

### scripts/update-flow-json.js

```javascript
#!/usr/bin/env node
const { parseArgs, readFlagJson } = require('./lib/cli');
const { platformRequest } = require('./lib/http');
const { run } = require('./lib/run');
const { requireFlowId } = require('./lib/whatsapp-flow');

run(async () => {
  const { flags } = parseArgs(process.argv.slice(2));
  const flowId = requireFlowId(flags);
  const flowJson = await readFlagJson(flags, 'json', 'json-file');

  if (!flowJson) {
    throw new Error('Missing --json or --json-file');
  }

  return platformRequest({
    method: 'POST',
    path: `/platform/v1/whatsapp/flows/${flowId}/versions`,
    body: {
      flow_json: flowJson
    }
  });
});

```

### scripts/publish-flow.js

```javascript
#!/usr/bin/env node
const { parseArgs, getStringFlag } = require('./lib/cli');
const { platformRequest } = require('./lib/http');
const { run } = require('./lib/run');
const { requireFlowId } = require('./lib/whatsapp-flow');

run(async () => {
  const { flags } = parseArgs(process.argv.slice(2));
  const flowId = requireFlowId(flags);
  const phoneNumberId = getStringFlag(flags, 'phone-number-id') || getStringFlag(flags, 'phone_number_id');
  const body = {};

  if (phoneNumberId) {
    body.phone_number_id = phoneNumberId;
  }

  return platformRequest({
    method: 'POST',
    path: `/platform/v1/whatsapp/flows/${flowId}/publish`,
    body: Object.keys(body).length > 0 ? body : undefined
  });
});

```

### scripts/send-test-flow.js

```javascript
#!/usr/bin/env node
const { parseArgs, requireStringFlag, getStringFlag, getBooleanFlag, readFlagJson } = require('./lib/cli');
const { metaRequest } = require('./lib/http');
const { run } = require('./lib/run');

run(async () => {
  const { flags } = parseArgs(process.argv.slice(2));
  const phoneNumberId = requireStringFlag(flags, 'phone-number-id');
  const to = requireStringFlag(flags, 'to');
  const flowId = requireStringFlag(flags, 'flow-id');
  const bodyText = requireStringFlag(flags, 'body-text');
  const flowCta = getStringFlag(flags, 'flow-cta') || 'Open';
  const headerText = getStringFlag(flags, 'header-text');
  const footerText = getStringFlag(flags, 'footer-text');
  const flowToken = getStringFlag(flags, 'flow-token') || flowId;
  const flowAction = getStringFlag(flags, 'flow-action');
  const flowActionPayload = await readFlagJson(flags, 'flow-action-payload', 'flow-action-payload-file');
  const mode = getStringFlag(flags, 'mode');
  const draft = getBooleanFlag(flags, 'draft');

  const payload = {
    messaging_product: 'whatsapp',
    recipient_type: 'individual',
    to,
    type: 'interactive',
    interactive: {
      type: 'flow',
      body: { text: bodyText },
      action: {
        name: 'flow',
        parameters: {
          flow_message_version: '3',
          flow_id: flowId,
          flow_cta: flowCta,
          flow_token: flowToken
        }
      }
    }
  };

  if (headerText) {
    payload.interactive.header = { type: 'text', text: headerText };
  }

  if (footerText) {
    payload.interactive.footer = { text: footerText };
  }

  if (flowAction) {
    payload.interactive.action.parameters.flow_action = flowAction;
  }

  if (flowActionPayload) {
    payload.interactive.action.parameters.flow_action_payload = flowActionPayload;
  }

  if (mode) {
    payload.interactive.action.parameters.mode = mode;
  } else if (draft) {
    payload.interactive.action.parameters.mode = 'draft';
  }

  return metaRequest({
    method: 'POST',
    path: `/${phoneNumberId}/messages`,
    body: payload
  });
});

```

### scripts/setup-encryption.js

```javascript
#!/usr/bin/env node
const { parseArgs, getStringFlag } = require('./lib/cli');
const { platformRequest } = require('./lib/http');
const { run } = require('./lib/run');
const { requireFlowId } = require('./lib/whatsapp-flow');

run(async () => {
  const { flags } = parseArgs(process.argv.slice(2));
  const flowId = requireFlowId(flags);
  const phoneNumberId = getStringFlag(flags, 'phone-number-id') || getStringFlag(flags, 'phone_number_id');

  const body = {};
  if (phoneNumberId) {
    body.phone_number_id = phoneNumberId;
  }

  return platformRequest({
    method: 'POST',
    path: `/platform/v1/whatsapp/flows/${flowId}/setup_encryption`,
    body: Object.keys(body).length > 0 ? body : undefined
  });
});

```

### scripts/set-data-endpoint.js

```javascript
#!/usr/bin/env node
const { parseArgs, readFlagText } = require('./lib/cli');
const { platformRequest } = require('./lib/http');
const { run } = require('./lib/run');
const { requireFlowId } = require('./lib/whatsapp-flow');

run(async () => {
  const { flags } = parseArgs(process.argv.slice(2));
  const flowId = requireFlowId(flags);
  const code = await readFlagText(flags, 'code', 'code-file');
  if (!code) {
    throw new Error('Missing --code or --code-file');
  }

  return platformRequest({
    method: 'POST',
    path: `/platform/v1/whatsapp/flows/${flowId}/data_endpoint`,
    body: { code }
  });
});

```