Skip to main content

Channels

WhatsApp

The WhatsApp adapter uses the official Meta WhatsApp Business Cloud API (graph.facebook.com/v18.0). Inbound messages arrive via Meta webhooks; outbound messages go through REST calls to the Cloud API.

Channel plugins read runtime config through app.channelService. The current EnvChannelService is env-backed and exposes one WhatsApp channel and one Telegram channel. The interface is ready for a DB-backed, multi-channel implementation later.

Architecture

src/channels/whatsapp/
├── index.ts # Service factory (WhatsAppService)
├── plugin.ts # Fastify plugin (routes)
├── connection.ts # Cloud API client, webhook parsing, token lifecycle
├── handler.ts # Transport normalization + AgentTask creation
├── sender.ts # Rate-limited outbound sender
├── queue.ts # BullMQ inbound/outbound workers
├── auth-state.ts # OAuth token persistence in Redis
├── conversation-repository.ts # Prisma conversation store
└── types.ts # Config and message types

Configuration

VariableDefaultDescription
WHATSAPP_APP_IDMeta app ID (used for token refresh)
WHATSAPP_APP_SECRETMeta app secret (used for token refresh)
WHATSAPP_PHONE_NUMBER_IDPhone number ID from Meta Business dashboard
WHATSAPP_ACCESS_TOKENInitial OAuth access token
WHATSAPP_WEBHOOK_VERIFY_TOKENWebhook verification token (set in Meta dashboard)
WA_PHONE_NUMBER+380000000000Sender phone number (used as Redis key prefix)
WA_RATE_LIMIT_PER_MIN60Max outbound messages per minute

Setup

  1. Create a Meta app with the WhatsApp Business product enabled.
  2. Add a phone number and note the Phone Number ID from the dashboard.
  3. Generate a long-lived access token and set WHATSAPP_ACCESS_TOKEN.
  4. Set a WHATSAPP_WEBHOOK_VERIFY_TOKEN and configure the webhook URL in the Meta dashboard:
    • Verification endpoint: GET /api/v1/whatsapp/webhook
    • Events endpoint: POST /api/v1/whatsapp/webhook
  5. Set the remaining env vars and start the server.

Connection Lifecycle

  1. Server boots and calls WhatsAppConnection.connect().
  2. The stored token is loaded from Redis. If missing, WHATSAPP_ACCESS_TOKEN is persisted to Redis.
  3. If the token is within 7 days of expiry, it is exchanged for a long-lived token via the Meta Graph API.
  4. A background check runs every 6 hours to refresh tokens proactively.
  5. On shutdown, the refresh scheduler is cleared.

Auth is handled entirely via OAuth tokens — no QR code scanning.

Message Flow

Inbound

  1. Meta sends a POST to /api/v1/whatsapp/webhook.
  2. connection.ts (handleWebhookPayload) parses the payload.
  3. Each message is extracted and dispatched to registered handlers.
  4. The handler enqueues the message to wa-inbound (BullMQ).
  5. The worker:
    • finds or creates a user and conversation from the WhatsApp JID;
    • stores the inbound message;
    • creates an AgentTask;
    • enqueues the agent-tasks worker.

The route returns { received: true } immediately; processing is async so we stay under Meta's response timeout.

The shared agent-tasks worker handles profile injection, RAG, generation, confidence checks, persona escalation, trust-matrix bypass, and HITL routing for both WhatsApp and Telegram.

Outbound

  1. Approved or bypassed message enqueued to wa-outbound.
  2. Worker calls WhatsAppSender.send().
  3. Token-bucket rate limiter waits for capacity.
  4. Cloud API POST /{phoneNumberId}/messages delivers the message.

Endpoints

MethodPathDescription
GET/api/v1/whatsapp/statusCloud API connection status
GET/api/v1/whatsapp/webhookMeta webhook verification (hub.challenge)
POST/api/v1/whatsapp/webhookInbound message ingestion from Meta

Message Formatting

The sender supports WhatsApp-native formatting:

  • *bold*
  • _italic_
  • ~strikethrough~
  • `code` and ```code block```

Rate Limiting

Per-sender token bucket. Configurable via WA_RATE_LIMIT_PER_MIN (default 60 msgs/min).

Token Storage

OAuth tokens live in Redis under key prefix <sessionKeyPrefix><phoneNumber>:. Two keys per token:

Key suffixValue
access_tokenCurrent access token string
token_expires_atUnix ms timestamp of expiry (empty string if unknown)

Tokens refresh proactively once they're within 7 days of expiry. To clear stored tokens (e.g. after revoking access), call clearTokenState(redis, keyPrefix) from auth-state.ts.


Telegram

The Telegram adapter uses Grammy — a Telegram Bot Framework for Node.js.

Architecture

src/channels/telegram/
├── index.ts # Service factory
├── plugin.ts # Fastify plugin (routes)
├── bot.ts # Grammy bot initialization
├── handler.ts # Transport normalization + AgentTask creation
├── sender.ts # Outbound message formatting
├── queue.ts # BullMQ inbound/outbound workers
├── conversation-repository.ts # Prisma conversation store
└── types.ts # Config and message types

Configuration

VariableDefaultDescription
TG_BOT_TOKENTelegram bot token from @BotFather
TG_RATE_LIMIT_PER_MIN30Max messages per minute

Setup

  1. Create a bot via @BotFather on Telegram
  2. Copy the bot token
  3. Set TG_BOT_TOKEN in .env
  4. Restart the server

The current plugin starts the bot in long-polling mode and exposes a status endpoint.

Message Flow

Inbound

  1. Grammy receives the message via long polling.
  2. Message enqueued to tg-inbound (BullMQ).
  3. The worker:
    • finds or creates a user and conversation from the Telegram user ID;
    • stores the inbound message;
    • creates an AgentTask;
    • enqueues the agent-tasks worker.

The shared agent-tasks worker decides whether to auto-send, create a HITL approval, or route to persona escalation.

Outbound

  1. Approved message enqueued to tg-outbound.
  2. Worker formats with Telegram MarkdownV2 or HTML.
  3. Sends via the Grammy bot API.
  4. Rate limiter throttles delivery.

Endpoints

MethodPathDescription
GET/api/v1/telegram/statusBot status

Message Formatting

Telegram supports:

  • **bold**
  • _italic_
  • `inline code`
  • Code blocks
  • HTML formatting as fallback

Conversation Tracking

Each Telegram user is keyed by tg_user_id. On first contact, the conversation repository creates User and Conversation records — same as the WhatsApp adapter.