> ## 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.

# Channels API

> Send outbound messages from any execute context — tools, jobs, webhooks, and triggers

## Overview

The Channels API lets your agent **initiate** messages on the channels it's connected to — WhatsApp, SMS, email, web chat, and more — from anywhere your code runs: a tool, a [scheduled job](/api/jobs), a [webhook](/api/luawebhook), or a [trigger](/api/luatool). It's the outbound half of a two-way conversation: inbound messages wake your agent, and `Channels.*` sends messages back out.

```typescript theme={null}
import { Channels } from 'lua-cli';

// Send a free-form message on any connected channel
await Channels.send({
  channel: 'whatsapp',
  to: { userId: 'user_123' },
  text: 'Your order has shipped! 📦'
});

// Send an email
await Channels.email.send({
  to: { email: 'customer@example.com' },
  subject: 'Order confirmation',
  text: 'Thanks for your order!'
});

// Re-open a closed WhatsApp window with an approved template
await Channels.whatsapp.sendTemplate({
  to: { phoneNumber: '+14155552671' },
  templateName: 'order_update',
  languageCode: 'en_US',
  messageContext: 'Told the customer their order shipped'
});
```

<Note>
  Every `Channels.*` send is **recorded to the recipient's conversation thread** with your agent. When the user replies, your agent picks up with full context — the outbound message is already part of the conversation it remembers. See [Proactive Messaging](/channels/proactive-messaging) for the full model.
</Note>

<CardGroup cols={2}>
  <Card title="Channels.send" icon="paper-plane">
    Free-form text on any connected channel
  </Card>

  <Card title="Channels.email.send" icon="envelope">
    Rich email — subject, HTML, cc/bcc, attachments
  </Card>

  <Card title="Channels.whatsapp.sendTemplate" icon="whatsapp" iconType="brands">
    Approved templates to start or re-open a conversation
  </Card>

  <Card title="Channels.whatsapp.sendReaction" icon="whatsapp" iconType="brands">
    React with an emoji to a specific WhatsApp message
  </Card>
</CardGroup>

## Where you can call it

`Channels.*` works in any execute context. Recipient resolution differs slightly by context:

| Context                 | Available? | Notes                                                                                          |
| ----------------------- | ---------- | ---------------------------------------------------------------------------------------------- |
| **Tools**               | ✅          | Has conversational context — you can target the current user with their `userId`, or any user. |
| **Jobs**                | ✅          | No conversational context — target an explicit `userId`, `phoneNumber`, or `email`.            |
| **Webhooks**            | ✅          | No conversational context — target an explicit recipient.                                      |
| **Triggers**            | ✅          | Same as webhooks — target an explicit recipient.                                               |
| **Pre/Post-processors** | ✅          | Available, but most sending happens in tools and jobs.                                         |

<Tip>
  Pair `Channels.send` with a [scheduled job](/api/jobs) for time-based outreach (reminders, follow-ups, digests) — see the [Proactive Send recipe](/examples/proactive-send).
</Tip>

## Channels.send()

Send a free-form text message on a connected channel.

```typescript theme={null}
Channels.send(input: ChannelSendInput): Promise<ChannelSendOutput>
```

<ParamField path="channel" type="string" required>
  The channel to send on. One of: `'whatsapp'`, `'sms'`, `'email'`, `'webchat'`, `'teams'`, `'instagram'`, `'messenger'`.
</ParamField>

<ParamField path="to" type="object" required>
  The recipient. Provide **exactly one** of the fields below.
</ParamField>

<ParamField path="to.userId" type="string">
  A Lua user ID. Works on **every** channel — the recipient's channel address is resolved from their conversation history with your agent.
</ParamField>

<ParamField path="to.phoneNumber" type="string">
  A phone number in E.164 format (e.g. `+14155552671`). Valid for **`whatsapp`** and **`sms`** only — lets you reach a number with no prior conversation (cold start).
</ParamField>

<ParamField path="to.email" type="string">
  An email address. Valid for the **`email`** channel only (cold start). For richer email, prefer [`Channels.email.send`](#channels-email-send).
</ParamField>

<ParamField path="text" type="string" required>
  The message text. Supports the same [response formatting components](/formatting/introduction) (`:::` blocks) as inline replies, where the channel renders them.
</ParamField>

<ParamField path="options" type="object">
  Optional send options — see [Options](#options).
</ParamField>

**Returns:** [`ChannelSendOutput`](#channelsendoutput)

**Examples:**

<Tabs>
  <Tab title="To the current user">
    ```typescript theme={null}
    // In a tool — message the user you're talking to, on a specific channel
    await Channels.send({
      channel: 'whatsapp',
      to: { userId: user._luaProfile.userId },
      text: 'Here is the summary you asked for.'
    });
    ```
  </Tab>

  <Tab title="Cold start (phone)">
    ```typescript theme={null}
    // Reach a phone number with no prior conversation (WhatsApp/SMS)
    await Channels.send({
      channel: 'sms',
      to: { phoneNumber: '+14155552671' },
      text: 'Your verification code is 123456.'
    });
    ```
  </Tab>

  <Tab title="From a job">
    ```typescript theme={null}
    import { Channels } from 'lua-cli';

    // In a scheduled job (no conversation context) — target an explicit recipient
    await Channels.send({
      channel: 'email',
      to: { email: 'customer@example.com' },
      text: 'Your weekly report is ready.'
    });
    ```
  </Tab>
</Tabs>

## Channels.email.send()

Send a rich email — subject, plain-text and/or HTML body, cc/bcc, and attachments.

```typescript theme={null}
Channels.email.send(input: EmailSendInput): Promise<ChannelSendOutput>
```

<ParamField path="to" type="object" required>
  The recipient. Provide **exactly one** of `to.userId` or `to.email`.
</ParamField>

<ParamField path="to.userId" type="string">
  A Lua user ID — the email address is resolved from the user's conversation history.
</ParamField>

<ParamField path="to.email" type="string">
  A literal email address (cold start).
</ParamField>

<ParamField path="subject" type="string">
  The email subject line.
</ParamField>

<ParamField path="text" type="string">
  Plain-text body, sent as-is (the plain-text MIME part). Provide at least one of `text`, `html`, or `richBody`.
</ParamField>

<ParamField path="html" type="string">
  HTML body, sent **as-is** — your exact markup, no template wrapper. Use this for designed / transactional emails.
</ParamField>

<ParamField path="richBody" type="string">
  Rich body — markdown and [`:::` component blocks](/formatting/introduction) **rendered server-side** into the branded email template (the same rendering your agent's inline replies use). Use this for "send this message as a nice email." Mutually exclusive with `html`.
</ParamField>

<ParamField path="cc" type="string[]">
  Carbon-copy recipients.
</ParamField>

<ParamField path="bcc" type="string[]">
  Blind-carbon-copy recipients.
</ParamField>

<ParamField path="attachments" type="EmailAttachmentInput[]">
  Files to attach. Each is `{ filename, contentType, url }` — the file is fetched from the public `url` at send time. Combined attachment size is capped at 28 MB.
</ParamField>

<ParamField path="inReplyTo" type="string">
  Threads this email as a reply (standard `In-Reply-To` header). Pass the `Message-ID` of the email you're replying to and mail clients group your message into that conversation — e.g. a thread per support ticket instead of one thread per user. When your agent handles an inbound email, the original `Message-ID` is available as `webhookPayload.messageId`; store it against your record and pass it back here on each follow-up. Honoured on the branded (existing-address) email channel.
</ParamField>

<ParamField path="references" type="string[]">
  The conversation's `References` chain (standard threading header) — the accumulated `Message-ID`s of the thread. Append the latest `Message-ID` each turn so long threads stay correctly linked.
</ParamField>

<ParamField path="options" type="object">
  Optional send options — see [Options](#options).
</ParamField>

**Returns:** [`ChannelSendOutput`](#channelsendoutput) (with `messageId` set to the provider message ID where available).

**Example:**

```typescript theme={null}
await Channels.email.send({
  to: { email: 'customer@example.com' },
  subject: 'Your invoice',
  html: '<h1>Invoice #12345</h1><p>Total: $99.00</p>',
  cc: ['accounts@example.com'],
  attachments: [
    {
      filename: 'invoice-12345.pdf',
      contentType: 'application/pdf',
      url: 'https://files.example.com/invoice-12345.pdf'
    }
  ]
});
```

Or let the platform render markdown into the branded template with `richBody`:

```typescript theme={null}
await Channels.email.send({
  to: { email: 'customer@example.com' },
  subject: 'Welcome aboard',
  richBody: '# Welcome!\n\nThanks for signing up — here is what to do next…'
});
```

**Threading a reply into an existing conversation** — keep follow-ups in the same email thread (e.g. one thread per ticket) by echoing the original message's `Message-ID`:

```typescript theme={null}
// `webhookPayload.messageId` is the inbound email's Message-ID — capture it
// when the ticket's first email arrives, then reuse it on every push.
await Channels.email.send({
  to: { email: 'tenant@example.com' },
  subject: 'Re: Porch light (MT-2026-184)',
  richBody: 'Your technician is booked for Thursday 9am.',
  inReplyTo: ticket.rootMessageId,
  references: [ticket.rootMessageId]
});
```

## Channels.whatsapp.sendTemplate()

Send a **pre-approved WhatsApp template**. Use this to start a conversation, or to reach a user whose [24-hour messaging window](/channels/proactive-messaging#the-whatsapp-24-hour-window) has closed (where free-form sends aren't allowed).

```typescript theme={null}
Channels.whatsapp.sendTemplate(input: WhatsAppTemplateSendInput): Promise<ChannelSendOutput>
```

<ParamField path="to" type="object" required>
  The recipient — exactly one of `to.userId` or `to.phoneNumber` (E.164).
</ParamField>

<ParamField path="templateName" type="string" required>
  The name of an approved template on the agent's WhatsApp channel. List available templates with [`Templates.whatsapp.list`](/api/templates#list).
</ParamField>

<ParamField path="languageCode" type="string">
  The template language, e.g. `'en_US'`.
</ParamField>

<ParamField path="components" type="object[]">
  Meta template component objects supplying the header / body / button parameter values — e.g. `{ type: 'BODY', parameters: [{ type: 'text', text: 'Tuesday at 3pm' }] }`. The component `type` is `'HEADER'`, `'BODY'`, or `'BUTTON'`; the parameter `type` is `'text'`, `'image'`, `'video'`, `'document'`, or `'coupon_code'`.
</ParamField>

<ParamField path="messageContext" type="string">
  A plain-text summary of what the template said. This is the text **recorded to the conversation thread** so your agent remembers the outreach when the user replies. Recommended whenever the template body isn't self-explanatory.
</ParamField>

<ParamField path="options" type="object">
  Optional send options — see [Options](#options).
</ParamField>

**Returns:** [`ChannelSendOutput`](#channelsendoutput)

**Example:**

```typescript theme={null}
await Channels.whatsapp.sendTemplate({
  to: { phoneNumber: '+14155552671' },
  templateName: 'appointment_reminder',
  languageCode: 'en_US',
  components: [
    { type: 'BODY', parameters: [{ type: 'text', text: 'Tuesday at 3pm' }] }
  ],
  messageContext: 'Reminded the customer about their Tuesday 3pm appointment'
});
```

<Note>
  `Channels.whatsapp.sendTemplate` is the **canonical** way to send a WhatsApp template as part of a conversation — it records the send to the recipient's thread and respects the recipient resolution above. The lower-level [`Templates.whatsapp.send`](/api/templates) (batch send by channel ID and phone numbers) remains available for bulk/campaign sends.
</Note>

## Channels.whatsapp.sendReaction()

React with an emoji to a specific WhatsApp message — the same reaction a person leaves by tapping and holding a message. This is also how the [Reaction formatting component](/formatting/reaction) sends on WhatsApp; call it directly when you need to react to a message outside of a normal reply (e.g. from a job or webhook).

```typescript theme={null}
Channels.whatsapp.sendReaction(input: WhatsAppReactionSendInput): Promise<ChannelSendOutput>
```

<ParamField path="to" type="object" required>
  The recipient — exactly one of `to.userId` or `to.phoneNumber` (E.164).
</ParamField>

<ParamField path="messageId" type="string" required>
  The vendor message ID of the WhatsApp message to react to (a `wamid....` value). Must be no more than 30 days old. Available from the channel webhook payload or the message's entry in conversation history.
</ParamField>

<ParamField path="emoji" type="string" required>
  A single emoji to react with. Pass an empty string (`''`) to remove the agent's existing reaction from the message.
</ParamField>

<ParamField path="options" type="object">
  Optional send options — see [Options](#options).
</ParamField>

**Returns:** [`ChannelSendOutput`](#channelsendoutput)

**Example:**

```typescript theme={null}
await Channels.whatsapp.sendReaction({
  to: { userId: 'user_123' },
  messageId: 'wamid.HBgLMTU1NTU1NTU1NTUVAgARGBI5QTNDQTVCM0Q0RUQ5RTU3RgA=',
  emoji: '👍'
});

// Remove the reaction
await Channels.whatsapp.sendReaction({
  to: { userId: 'user_123' },
  messageId: 'wamid.HBgLMTU1NTU1NTU1NTUVAgARGBI5QTNDQTVCM0Q0RUQ5RTU3RgA=',
  emoji: ''
});
```

<Warning>
  **WhatsApp only, for now.** `sendReaction` isn't available on other channels — use the [Reaction formatting component](/formatting/reaction) for the broader multi-channel path (Instagram, Facebook Messenger, Slack, iMessage, web chat), which is ignored on channels without reaction support rather than throwing.
</Warning>

## Options

All three methods accept an optional `options` object:

<ParamField path="options.channelIdentifier" type="string">
  Pin the send to a specific channel configuration (for agents with more than one channel of the same type). The configuration must belong to your agent.
</ParamField>

<ParamField path="options.whatsapp.onClosedWindow" type="'queue' | 'fail'" default="'queue'">
  What to do when a WhatsApp free-form send hits a closed [24-hour window](/channels/proactive-messaging#the-whatsapp-24-hour-window):

  * **`'queue'`** (default) — queue the message and deliver it after the user re-engages (the API returns `queued: true`).
  * **`'fail'`** — reject the send so you can fall back to `Channels.whatsapp.sendTemplate`.
</ParamField>

## ChannelSendOutput

Every send method resolves to the same shape:

```typescript theme={null}
interface ChannelSendOutput {
  delivered: boolean;     // the channel accepted the message for delivery
  persisted: boolean;     // the message was recorded to the agent's conversation thread
  queued?: boolean;       // WhatsApp only: deferred until the recipient re-engages
  userId?: string;        // the Lua user the message was recorded against
  identifier?: string;    // the channel-native recipient (phone / email / etc.)
  messageId?: string;     // provider message ID, where available
  warning?: string;       // set when delivered but not persisted
}
```

<ParamField path="delivered" type="boolean">
  The channel accepted the message. Independent of `persisted`.
</ParamField>

<ParamField path="persisted" type="boolean">
  The message was recorded to the recipient's conversation thread with your agent. A delivered-but-unpersisted send returns `delivered: true, persisted: false` plus a `warning` — it does **not** throw.
</ParamField>

<ParamField path="queued" type="boolean">
  WhatsApp only. `true` means the 24-hour window was closed and the message is queued — `delivered` stays `false` until the recipient re-engages, at which point the queued text is delivered and recorded. See [Proactive Messaging](/channels/proactive-messaging#the-whatsapp-24-hour-window).
</ParamField>

## Error handling

`Channels.*` **throws** when a send is rejected (invalid recipient, unconfigured channel, provider rejection, or `onClosedWindow: 'fail'` on a closed window). Wrap calls in `try/catch` where a failure should be handled gracefully.

A successful call may still report partial success — **`delivered: true` with `persisted: false`** (plus a `warning`) means the message went out but couldn't be recorded to the conversation thread. This is **not** an error and won't throw; check the flag if recording matters to your flow.

```typescript theme={null}
try {
  const result = await Channels.send({
    channel: 'whatsapp',
    to: { userId: 'user_123' },
    text: 'Quick update for you!'
  });

  if (result.queued) {
    // Window closed — message will deliver when the user replies
    console.log('Queued; will deliver on re-engagement');
  } else if (!result.persisted) {
    console.warn('Delivered but not recorded:', result.warning);
  }
} catch (err) {
  // Rejected — e.g. unconfigured channel, invalid recipient, closed-window 'fail'
  console.error('Send failed:', err.message);
}
```

<Warning>
  **WhatsApp free-form sending is time-limited.** You can only send free-form WhatsApp messages within 24 hours of the recipient's last inbound message. Outside that window, use `Channels.whatsapp.sendTemplate` with an approved template — or rely on the default `onClosedWindow: 'queue'` behavior. See [Proactive Messaging](/channels/proactive-messaging) and [Channel Capabilities](/channels/channel-capabilities).
</Warning>

## Channels & recipients at a glance

| Channel     | Cold start (no prior conversation)                       | Warm only (`userId`) |
| ----------- | -------------------------------------------------------- | -------------------- |
| `whatsapp`  | ✅ via `phoneNumber` (template required if window closed) | ✅                    |
| `sms`       | ✅ via `phoneNumber`                                      | ✅                    |
| `email`     | ✅ via `email`                                            | ✅                    |
| `webchat`   | —                                                        | ✅                    |
| `teams`     | —                                                        | ✅                    |
| `instagram` | —                                                        | ✅                    |
| `messenger` | —                                                        | ✅                    |

See [Channel Capabilities](/channels/channel-capabilities) for per-channel limits, sender resolution, and compliance notes.

## TypeScript types

```typescript theme={null}
type ChannelSendChannel =
  | 'whatsapp' | 'sms' | 'email' | 'webchat'
  | 'teams' | 'instagram' | 'messenger';

interface ChannelSendTarget {
  userId?: string;       // any channel
  phoneNumber?: string;  // whatsapp / sms (cold start)
  email?: string;        // email (cold start)
}

interface ChannelSendOptions {
  channelIdentifier?: string;
  whatsapp?: { onClosedWindow?: 'queue' | 'fail' };
}

interface ChannelSendInput {
  channel: ChannelSendChannel;
  to: ChannelSendTarget;
  text: string;
  options?: ChannelSendOptions;
}

interface WhatsAppTemplateSendInput {
  to: { userId?: string; phoneNumber?: string };
  templateName: string;
  languageCode?: string;
  components?: Array<Record<string, unknown>>;
  messageContext?: string;
  options?: ChannelSendOptions;
}

interface WhatsAppReactionSendInput {
  to: { userId?: string; phoneNumber?: string };
  messageId: string;
  emoji: string;
  options?: ChannelSendOptions;
}

interface EmailAttachmentInput {
  filename: string;
  contentType: string;
  url: string;
}

interface EmailSendInput {
  to: { userId?: string; email?: string };
  subject?: string;
  text?: string;
  html?: string;
  richBody?: string;
  cc?: string[];
  bcc?: string[];
  attachments?: EmailAttachmentInput[];
  inReplyTo?: string;
  references?: string[];
  options?: ChannelSendOptions;
}

interface ChannelSendOutput {
  delivered: boolean;
  persisted: boolean;
  queued?: boolean;
  userId?: string;
  identifier?: string;
  messageId?: string;
  warning?: string;
}
```

## Next steps

<CardGroup cols={2}>
  <Card title="Proactive Messaging" icon="bell" href="/channels/proactive-messaging">
    The full model: Channels.send vs user.send() vs templates, and per-channel windows
  </Card>

  <Card title="Channel Capabilities" icon="table-list" href="/channels/channel-capabilities">
    Per-channel limits, sender resolution, and compliance
  </Card>

  <Card title="Proactive Send Recipe" icon="clock" href="/examples/proactive-send">
    Schedule outreach with defineJob + Channels.send
  </Card>

  <Card title="Templates API" icon="whatsapp" iconType="brands" href="/api/templates">
    List and batch-send approved WhatsApp templates
  </Card>
</CardGroup>
