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

# Templates

> List, read, and batch-send approved WhatsApp message templates by channel id

`Templates.whatsapp` lists the [WhatsApp message templates](/channels/whatsapp) on one of the agent's WhatsApp channels and sends one to a list of phone numbers. To send a template to one end user as part of their conversation, with a delivery record, use [`Channels.whatsapp.sendTemplate`](/reference/sdk/channels) instead. Marketplace agent templates are a different thing; see [`lua marketplace`](/reference/cli/marketplace). Available in tools, jobs, webhooks, triggers, processors, and workflow code steps.

*Verified against lua-cli 3.33.0.*

```ts theme={null}
import { Templates } from 'lua-cli';
```

## Quick example

Find the channel id with `lua channels list`, then pass it to every call.

```ts theme={null}
import { Templates, env } from 'lua-cli';

const channelId = env('WHATSAPP_CHANNEL_ID') ?? '';
const page = await Templates.whatsapp.list(channelId, { search: 'order' });

const result = await Templates.whatsapp.send(channelId, 'order_confirmation', {
  phoneNumbers: ['+15551234567'],
  values: { body: { first_name: 'Ada', order_number: 'ORD-4471' } },
});
```

## Methods

### whatsapp.list(channelId, options?)

Lists the channel's templates, paginated, optionally filtered by name.

```ts theme={null}
Templates.whatsapp.list(channelId: string, options?: ListTemplatesOptions): Promise<PaginatedTemplatesResponse>
```

<ParamField path="channelId" type="string" required>
  The WhatsApp channel identifier from `lua channels list`.
</ParamField>

<ParamField path="options.page" type="number" default={1}>
  Page number, starting at 1.
</ParamField>

<ParamField path="options.limit" type="number" default={10}>
  Templates per page.
</ParamField>

<ParamField path="options.search" type="string">
  Filters templates by name.
</ParamField>

**Returns**

<ResponseField name="page" type="PaginatedTemplatesResponse">
  <Expandable title="properties">
    <ResponseField name="templates" type="WhatsAppTemplate[]">The templates on this page.</ResponseField>
    <ResponseField name="total" type="number">Matching templates across all pages.</ResponseField>
    <ResponseField name="page" type="number">The page returned.</ResponseField>
    <ResponseField name="limit" type="number">The page size used.</ResponseField>
    <ResponseField name="totalPages" type="number">Number of pages.</ResponseField>
  </Expandable>
</ResponseField>

**Example**

```ts theme={null}
import { Templates, env } from 'lua-cli';

const channelId = env('WHATSAPP_CHANNEL_ID') ?? '';
const approved = (await Templates.whatsapp.list(channelId, { limit: 50 })).templates.filter(
  (t) => t.status === 'APPROVED',
);
```

**Errors** — `Failed to list templates` when the platform gives no message.

### whatsapp.get(channelId, templateId)

Reads one template with its components.

```ts theme={null}
Templates.whatsapp.get(channelId: string, templateId: string): Promise<WhatsAppTemplate>
```

<ParamField path="channelId" type="string" required>
  The WhatsApp channel identifier.
</ParamField>

<ParamField path="templateId" type="string" required>
  Meta's numeric template id, or the template's exact name (case-insensitive).
</ParamField>

**Returns** — a `WhatsAppTemplate`.

**Example**

```ts theme={null}
import { Templates, env } from 'lua-cli';

const channelId = env('WHATSAPP_CHANNEL_ID') ?? '';
const template = await Templates.whatsapp.get(channelId, 'order_confirmation');
if (template.status !== 'APPROVED') {
  throw new Error(`${template.name} is ${template.status}`);
}
```

**Errors** — `Template "<name>" not found. Use a numeric template ID or an exact template name.`; `Failed to get template` when the platform gives no message.

### whatsapp.send(channelId, templateId, data)

Sends one approved template to a list of phone numbers and reports the outcome per recipient.

```ts theme={null}
Templates.whatsapp.send(channelId: string, templateId: string, data: SendTemplateData): Promise<SendTemplateResponse>
```

<ParamField path="channelId" type="string" required>
  The WhatsApp channel identifier.
</ParamField>

<ParamField path="templateId" type="string" required>
  Meta's numeric template id, or the template's exact name (case-insensitive). The template must be `APPROVED`.
</ParamField>

<ParamField path="data.phoneNumbers" type="string[]" required>
  Recipients in E.164 form. A leading `+`, spaces, and dashes are stripped; entries with no digits are dropped.
</ParamField>

<ParamField path="data.values.header" type="Record<string, string>">
  Header parameters. For a media header, pass the public HTTPS URL as `image_url`, `video_url`, or `document_url` (plus `document_filename`), or a Meta media id as `image_id`, `video_id`, or `document_id`. A media id wins when both are given.
</ParamField>

<ParamField path="data.values.body" type="Record<string, string>">
  Body parameters keyed by the template's parameter names.
</ParamField>

<ParamField path="data.values.buttons" type="Array<{ sub_type, index, text?, coupon_code? }>">
  Button parameters; `sub_type` is `QUICK_REPLY`, `URL`, `PHONE_NUMBER`, or `COPY_CODE`, and `index` the button's position as a string.
</ParamField>

**Returns**

<ResponseField name="result" type="SendTemplateResponse">
  Recipients are processed in batches of 10 with a one-second pause between batches and 30 seconds per recipient. Entries keep recipient order but don't carry the phone number; send one recipient per call when you need to correlate.

  <Expandable title="properties">
    <ResponseField name="results" type="any[]">One entry per recipient the provider answered: Meta's response object. On success `messages[0].id` is the WhatsApp message id (`wamid…`); a recipient Meta rejected appears here with an `error` object instead.</ResponseField>
    <ResponseField name="errors" type="any[]">One entry per recipient whose send failed before the provider answered; read `message`.</ResponseField>
    <ResponseField name="totalProcessed" type="number">`results.length`.</ResponseField>
    <ResponseField name="totalErrors" type="number">`errors.length`.</ResponseField>
  </Expandable>
</ResponseField>

**Example**

```ts theme={null}
import { LuaTool, Templates, env } from 'lua-cli';
import { z } from 'zod';

export default class SendOrderConfirmationTool implements LuaTool {
  name = 'send_order_confirmation';
  description = 'Send the order confirmation template to a customer';
  inputSchema = z.object({
    phoneNumber: z.string().describe('E.164 phone number'),
    orderNumber: z.string(),
    customerName: z.string(),
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const channelId = env('WHATSAPP_CHANNEL_ID') ?? '';
    const result = await Templates.whatsapp.send(channelId, 'order_confirmation', {
      phoneNumbers: [input.phoneNumber],
      values: { body: { customer_name: input.customerName, order_number: input.orderNumber } },
    });
    if (result.totalErrors > 0) {
      return { sent: false, error: String(result.errors[0]?.message ?? 'send failed') };
    }
    return { sent: true, messageId: result.results[0]?.messages?.[0]?.id };
  }
}
```

**Errors** — `Template must be approved before sending`; `Template "<name>" not found. Use a numeric template ID or an exact template name.`; `No valid recipient phone numbers (numbers must contain digits)`; a 400 naming the component when a parameter value is missing or extra; `Failed to send template` when the platform gives no message.

## Types

<ResponseField name="ListTemplatesOptions" type="interface">
  `page?`, `limit?`, `search?`.
</ResponseField>

<ResponseField name="PaginatedTemplatesResponse" type="interface">
  The result of `list`.
</ResponseField>

<ResponseField name="WhatsAppTemplate" type="interface">
  <Expandable title="properties">
    <ResponseField name="id" type="string">Meta's numeric id.</ResponseField>
    <ResponseField name="name" type="string">The template name.</ResponseField>
    <ResponseField name="status" type="WhatsAppTemplateStatus">`APPROVED`, `PENDING`, or `REJECTED`.</ResponseField>
    <ResponseField name="category" type="WhatsAppTemplateCategory">`AUTHENTICATION`, `MARKETING`, or `UTILITY`.</ResponseField>
    <ResponseField name="language" type="string">The language code, for example `en_US`.</ResponseField>
    <ResponseField name="components" type="WhatsAppTemplateComponent[]">`HEADER` (with `format` `TEXT`, `IMAGE`, `VIDEO`, or `DOCUMENT`), `BODY`, `FOOTER`, and `BUTTONS` components.</ResponseField>
    <ResponseField name="parameter_format" type="'NAMED' | 'POSITIONAL'">How the template names its parameters.</ResponseField>
    <ResponseField name="rejected_reason, previous_category, correct_category, message_send_ttl_seconds" type="string | number">Meta review metadata, when present.</ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="SendTemplateData" type="interface">
  `phoneNumbers` and `values?` (a `SendTemplateValues`), as on `send`.
</ResponseField>

<ResponseField name="SendTemplateResponse" type="interface">
  The result of `send`. `results` and `errors` are typed `any[]`.
</ResponseField>

## See also

* [`Channels`](/reference/sdk/channels) — send a template to one end user with a delivery record
* [WhatsApp setup](/channels/whatsapp) — connect a number and get templates approved
* [Send proactive messages](/build/send-proactive-messages) — the 24-hour window and when templates are required
* [`lua channels`](/reference/cli/channels) — find the channel identifier
