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

# Channels SDK

> Connect messaging channels and manage channel privacy

`rt.channels` manages first-class agent channels such as Telegram and WhatsApp. Use it with `rt.secrets` to store provider credentials and then connect the channel to an agent.

## Connect Telegram

```typescript theme={null}
await rt.secrets.set('TELEGRAM_GMAIL_AGENT_BOT_TOKEN', process.env.TELEGRAM_BOT_TOKEN!, {
  orgWide: true,
  category: 'tool',
  description: 'Telegram bot token for gmail-agent',
});

const channel = await rt.channels.connectTelegram({
  agent: 'gmail-agent',
  botTokenSecretName: 'TELEGRAM_GMAIL_AGENT_BOT_TOKEN',
});

await rt.channels.registerWebhook(channel.id);
```

Each Telegram bot can be connected to one agent at a time.

## Connect WhatsApp

```typescript theme={null}
await rt.secrets.set('WHATSAPP_GMAIL_AGENT_ACCESS_TOKEN', process.env.WHATSAPP_ACCESS_TOKEN!, {
  orgWide: true,
  category: 'tool',
  description: 'WhatsApp Cloud API token for gmail-agent',
});

await rt.secrets.set('WHATSAPP_GMAIL_AGENT_APP_SECRET', process.env.WHATSAPP_APP_SECRET!, {
  orgWide: true,
  category: 'tool',
  description: 'Meta app secret for gmail-agent WhatsApp',
});

await rt.channels.validateWhatsAppConnection({
  agent: 'gmail-agent',
  accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
  phoneNumberId: '123456789012345',
});

const whatsapp = await rt.channels.connectWhatsApp({
  agent: 'gmail-agent',
  accessTokenSecretName: 'WHATSAPP_GMAIL_AGENT_ACCESS_TOKEN',
  appSecretSecretName: 'WHATSAPP_GMAIL_AGENT_APP_SECRET',
  phoneNumberId: '123456789012345',
  businessAccountId: '987654321098765',
});

const registered = await rt.channels.registerWebhookWithSetup(whatsapp.id);
console.log(registered.setup?.webhookUrl);
console.log(registered.setup?.verifyToken);
```

Paste the returned callback URL and verify token into the Meta app's WhatsApp webhook settings. Each WhatsApp Business phone number can be connected to one agent at a time.

## Privacy

Channels default to `approved_only`. Unknown chats become pending requests and do not reach the agent.

```typescript theme={null}
await rt.channels.updatePrivacy(channel.id, {
  mode: 'approved_only',
  allowPrivateChats: true,
  allowGroups: true,
  unauthorizedReply: 'message',
});
```

Approve a pending Telegram chat:

```typescript theme={null}
const state = await rt.channels.getPrivacy(channel.id);
const request = state.requests[0];

await rt.channels.approveAccess(channel.id, {
  requestId: request.id,
});
```

Approve a known conversation directly:

```typescript theme={null}
await rt.channels.approveAccess(channel.id, {
  externalConversationId: 'telegram:chat:123456789',
  conversationType: 'private',
  conversationDisplayName: 'Arthur (@ArtKara1)',
});
```

Revoke access:

```typescript theme={null}
const { grants } = await rt.channels.getPrivacy(channel.id);
await rt.channels.removeAccessGrant(channel.id, grants[0].id);
```

## Product channels

Use product channels when a product built on Runtools needs its own users to chat with an agent over Telegram, WhatsApp, Slack, Discord, iMessage, or another supported channel.

The developer connects the channel once. End users do not connect Telegram or create Runtools accounts. Instead, Runtools links the provider sender identity, such as a Telegram user id or phone number, to your product user:

```text theme={null}
appId + externalUserId + agentChannelId + provider sender id
```

Once linked, channel-triggered agent runs use the same Hosted Connect end-user scope as your web app. If that product user has not connected Gmail, Stripe, Slack, or another data integration, tool execution returns `connect_required`. It never falls back to builder Connected Apps or org credentials.

### Bind a channel to a product app

Create the agent channel normally, then bind it to the product app:

```typescript theme={null}
const channel = await rt.channels.connectTelegram({
  agent: 'atlas-agent',
  botTokenSecretName: 'TELEGRAM_ATLAS_BOT_TOKEN',
});

await rt.channels.registerWebhook(channel.id);

await rt.channels.bindProductChannel('atlas', {
  agentChannelId: channel.id,
  identityPolicy: 'link_required',
  groupPolicy: 'sender_scoped',
  unknownSenderReply:
    'Open Atlas to link this chat: {linkUrl}',
  metadata: {
    linkUrl: 'https://atlas.example.com/settings/channels/link',
  },
});
```

`groupPolicy: 'disabled'` blocks group chats for product channels. `sender_scoped` lets the same group contain multiple product users because each message is resolved by sender id.

### Product-started linking

When a signed-in product user clicks "Connect Telegram" or "Connect WhatsApp" inside your product, create a short-lived channel link session from your server:

```typescript theme={null}
const session = await rt.channels.createProductLinkSession({
  appId: 'atlas',
  agentChannelId: channel.id,
  externalUserId: currentUser.id,
  email: currentUser.email,
  name: currentUser.name,
  returnUrl: 'https://atlas.example.com/settings/channels',
});

return Response.json({
  linkUrl: session.linkUrl,
  token: session.token,
});
```

After your product verifies the provider sender id, complete the session from your server:

```typescript theme={null}
await rt.channels.completeProductLinkSession({
  appId: 'atlas',
  sessionId: session.id,
  token: session.token!,
  productExternalUserId: currentUser.id,
  providerExternalUserId: telegramUserId,
  username: telegramUsername,
  displayName: telegramDisplayName,
});
```

For backend imports, admin tooling, or provider webhooks where you already trust the sender identity, link directly:

```typescript theme={null}
await rt.channels.linkProductChannelUser({
  appId: 'atlas',
  agentChannelId: channel.id,
  externalUserId: telegramUserId,
  productExternalUserId: currentUser.id,
  username: telegramUsername,
  displayName: telegramDisplayName,
});
```

### Channel-started linking

If an unknown person messages a product-bound channel, Runtools creates a pending link session and replies with the configured link URL. The message is recorded for audit, but the agent is not called until the sender is linked to a product user.

List and revoke links:

```typescript theme={null}
const links = await rt.channels.listProductLinks('atlas', {
  agentChannelId: channel.id,
  productExternalUserId: currentUser.id,
});

await rt.channels.revokeProductLink('atlas', links[0].id);
```

## Methods

| Method                                                | Description                                                                                   |
| ----------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `rt.channels.list({ agent?, provider? })`             | List channel connections                                                                      |
| `rt.channels.connectTelegram(input)`                  | Connect a BYOK Telegram bot token secret to an agent                                          |
| `rt.channels.connectWhatsApp(input)`                  | Connect a BYOK WhatsApp phone number to an agent                                              |
| `rt.channels.validateWhatsAppConnection(input)`       | Validate a WhatsApp token and phone number before saving secrets                              |
| `rt.channels.registerWebhook(channelId)`              | Register the provider webhook and return the channel                                          |
| `rt.channels.registerWebhookWithSetup(channelId)`     | Register the provider webhook and return one-time setup values such as WhatsApp verify tokens |
| `rt.channels.disconnect(channelId)`                   | Disconnect a channel and clean up provider webhook state                                      |
| `rt.channels.getPrivacy(channelId)`                   | Read policy, pending requests, and approved grants                                            |
| `rt.channels.updatePrivacy(channelId, patch)`         | Update channel privacy policy                                                                 |
| `rt.channels.listAccessRequests(channelId, status?)`  | List access requests                                                                          |
| `rt.channels.approveAccess(channelId, input)`         | Approve a pending request or known conversation                                               |
| `rt.channels.denyAccessRequest(channelId, requestId)` | Deny a pending request                                                                        |
| `rt.channels.removeAccessGrant(channelId, grantId)`   | Revoke an approved conversation                                                               |
| `rt.channels.listProductBindings(appId)`              | List product bindings for channels                                                            |
| `rt.channels.bindProductChannel(appId, input)`        | Bind a developer-owned channel to a product app                                               |
| `rt.channels.unbindProductChannel(appId, bindingId)`  | Disable a product channel binding                                                             |
| `rt.channels.createProductLinkSession(input)`         | Create a short-lived channel identity link session                                            |
| `rt.channels.completeProductLinkSession(input)`       | Complete a pending link session after product auth                                            |
| `rt.channels.listProductLinks(appId, filters?)`       | List linked channel identities for a product                                                  |
| `rt.channels.linkProductChannelUser(input)`           | Directly link a channel sender to a product user                                              |
| `rt.channels.revokeProductLink(appId, linkId)`        | Revoke a product channel identity link                                                        |
