Skip to main content

Norns WhatsApp Channel

WhatsApp integration for Norns via Twilio API, enabling conversational AI interactions and proactive messaging through WhatsApp.

Overview

The WhatsApp channel allows users to interact with Norns through WhatsApp messages. It supports:

  • Inbound messaging: Users can message Norns to create tasks, get briefings, check schedules
  • Rich messaging: Interactive buttons and list menus (framework ready)
  • Proactive messaging: Scheduled reminders, daily briefings, calendar alerts via templates
  • 24-hour session windows: Automatic tracking for WhatsApp's messaging rules
  • Multi-channel continuity: Conversations persist across Slack, Voice, and WhatsApp

Architecture

┌─────────────────────────────────────────────────────────────────────┐
│ WhatsApp Channel Flow │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ User Phone │
│ │ │
│ ▼ │
│ ┌─────────┐ ┌─────────────┐ ┌──────────────────────┐ │
│ │WhatsApp │────▶│ Twilio │────▶│ /whatsapp/webhook │ │
│ │ App │ │ (Sandbox) │ │ (Norns Agent) │ │
│ └─────────┘ └─────────────┘ └──────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ WhatsAppClient │ │
│ │ - verify_webhook │ │
│ │ - parse message │ │
│ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ resolve_user_by │ │
│ │ _phone() │ │
│ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ SessionTracker │ │
│ │ (24h window) │ │
│ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ process_message │ │
│ │ (LangGraph) │ │
│ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ MessageBuilder │ │
│ │ (format response)│ │
│ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────┐ ┌─────────────┐ ┌──────────────────┐ │
│ │WhatsApp │◀────│ Twilio │◀────│ send_text() │ │
│ │ App │ │ Messages │ │ send_template() │ │
│ └─────────┘ └─────────────┘ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

Module Structure

services/norns/agent/whatsapp/
├── __init__.py # Module exports
├── client.py # Twilio WhatsApp API wrapper
├── message_builder.py # Rich message formatting
├── template_manager.py # Proactive template handling
└── session_tracker.py # 24-hour window management

client.py - WhatsAppClient

Wrapper for Twilio WhatsApp API with methods for:

MethodDescription
verify_webhook(url, params, signature)Verify Twilio webhook signature
send_text(to_number, body)Send plain text message
send_interactive(to_number, content_sid, variables)Send interactive message
send_media(to_number, media_url, caption)Send media message
send_template(to_number, content_sid, variables)Send template message

message_builder.py - WhatsAppMessageBuilder

Converts agent responses to WhatsApp-appropriate formats:

Message TypeUse CaseLimits
TEXTSimple responses4096 chars
QUICK_REPLYYes/No, confirmationsMax 3 buttons, 20 chars each
LISTTask lists, menusMax 10 sections, 10 rows each
MEDIAImages, documentsVaries by type
TEMPLATEProactive messagesPre-approved only

template_manager.py - TemplateManager

Manages pre-approved WhatsApp templates for proactive messaging:

# Usage
await template_manager.send_template(
to_number="+1234567890",
template_name="task_reminder",
variables={"task_name": "Review PR", "due_time": "2 hours"}
)

session_tracker.py - WhatsAppSessionTracker

Tracks 24-hour customer service windows per phone number:

# Check window status
in_window = await tracker.is_in_session_window("+1234567890")

# Record inbound (extends window)
await tracker.record_inbound(user_id, "+1234567890")

# Get detailed info
info = await tracker.get_session_info("+1234567890")
# Returns: {in_window, expires_at, time_remaining, last_inbound}

API Endpoints

Webhooks (Twilio → Norns)

EndpointMethodDescription
/whatsapp/webhookPOSTReceive inbound WhatsApp messages
/whatsapp/statusPOSTReceive delivery status callbacks

Webhook Payload Fields:

FieldDescription
FromSender phone (whatsapp:+1234567890)
ToYour Twilio number
BodyMessage text
MessageSidUnique message ID
ButtonTextQuick reply button text (if tapped)
ButtonPayloadCustom payload from button
NumMediaNumber of attachments
MediaUrl0First media URL

Internal API (Authenticated)

EndpointMethodDescription
/api/whatsapp/sendPOSTSend proactive template message
/api/whatsapp/templatesGETList configured templates
/api/whatsapp/session/{phone}GETCheck session window status

Send Proactive Message:

curl -X POST "https://norns-pm.ravenhelm.dev/api/whatsapp/send" \
-H "X-API-Key: $NORNS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "701973d2-57e4-4c84-a2ec-ded996dcf676",
"template_name": "task_reminder",
"variables": {
"task_name": "Review quarterly report",
"due_time": "in 2 hours"
}
}'

Response:

{"success": true, "message_sid": "SM1234567890abcdef"}

Configuration

Environment Variables

Located in /Users/ravenhelm/ravenhelm/secrets/.env:

# Twilio credentials (shared with telephony)
TWILIO_ACCOUNT_SID=AC94d0db92da17f5a5b25dbacc7389dae6
TWILIO_AUTH_TOKEN=e5ab75afa96d0ef3e217dd64c5603a1e

# WhatsApp-specific
TWILIO_WHATSAPP_NUMBER=+14155238886

# Content Template SIDs
WA_TEMPLATE_TASK_REMINDER=HX685da4c35863ed1180fb46bb8e99004b
WA_TEMPLATE_DAILY_BRIEFING=HX47ad70041318b7e3572e586c04a1b4a8
WA_TEMPLATE_CALENDAR_REMINDER=HX657879ccc90ffda83f6648d809febdea
WA_TEMPLATE_TASK_COMPLETE=HX1cf5d904e8e55f6adeb4e59cc9efada1
WA_TEMPLATE_WEEKLY_REVIEW=HX6680d9a8507e9fe8e7a67e1a83c53a5e

Docker Compose

In services/norns/docker-compose.yml:

environment:
# Twilio/WhatsApp
- TWILIO_ACCOUNT_SID=${TWILIO_ACCOUNT_SID}
- TWILIO_AUTH_TOKEN=${TWILIO_AUTH_TOKEN}
- TWILIO_WHATSAPP_NUMBER=${TWILIO_WHATSAPP_NUMBER}
- WHATSAPP_STATUS_CALLBACK_URL=https://norns-pm.ravenhelm.dev/whatsapp/status
# WhatsApp Templates
- WA_TEMPLATE_TASK_REMINDER=${WA_TEMPLATE_TASK_REMINDER:-}
- WA_TEMPLATE_DAILY_BRIEFING=${WA_TEMPLATE_DAILY_BRIEFING:-}
- WA_TEMPLATE_CALENDAR_REMINDER=${WA_TEMPLATE_CALENDAR_REMINDER:-}
- WA_TEMPLATE_TASK_COMPLETE=${WA_TEMPLATE_TASK_COMPLETE:-}
- WA_TEMPLATE_WEEKLY_REVIEW=${WA_TEMPLATE_WEEKLY_REVIEW:-}

Database Schema

Migration: 012_whatsapp_sessions.sql

-- WhatsApp session window tracking
CREATE TABLE whatsapp_sessions (
session_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
phone_number VARCHAR(20) NOT NULL UNIQUE,
window_expires_at TIMESTAMPTZ,
last_inbound_at TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_whatsapp_sessions_user ON whatsapp_sessions(user_id);
CREATE INDEX idx_whatsapp_sessions_expires ON whatsapp_sessions(window_expires_at);

-- Users table phone column
ALTER TABLE users ADD COLUMN IF NOT EXISTS phone_number VARCHAR(20);
CREATE INDEX idx_users_phone ON users(phone_number) WHERE phone_number IS NOT NULL;

Message Templates

WhatsApp requires pre-approved templates for proactive messaging outside the 24-hour session window.

Configured Templates

TemplateSIDVariablesMessage
task_reminderHX685da4c35863ed1180fb46bb8e99004btask_name, due_time"Reminder: {{1}} is due {{2}}. Reply to take action."
daily_briefingHX47ad70041318b7e3572e586c04a1b4a8date, task_count, top_tasks"Good morning! Here's your briefing for {{1}}:..."
calendar_reminderHX657879ccc90ffda83f6648d809febdeaevent_name, time, location"Upcoming: {{1}} at {{2}}\nLocation: {{3}}"
task_complete_confirmHX1cf5d904e8e55f6adeb4e59cc9efada1task_name"Task completed: {{1}}"
weekly_reviewHX6680d9a8507e9fe8e7a67e1a83c53a5ecompleted_count, highlights"Weekly Review: You completed {{1}} tasks..."

Creating New Templates

Using Twilio CLI/API:

# Create template
curl -X POST "https://content.twilio.com/v1/Content" \
-u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"friendly_name": "new_template",
"language": "en",
"variables": {"1": "var_name"},
"types": {
"twilio/text": {
"body": "Message with `{{1}}`"
}
}
}'

# List templates
twilio api:content:v1:content:list

# Get template details
twilio api:content:v1:content:fetch --sid HXXXXXXXXXXX

24-Hour Session Window

WhatsApp enforces a customer service window rule:

  1. Window Opens: When user sends an inbound message
  2. Window Duration: 24 hours from last inbound message
  3. Inside Window: Can send free-form messages
  4. Outside Window: Must use approved templates only

Session Flow

User sends message


┌─────────────────┐
│ record_inbound()│ ──── Updates window_expires_at = NOW() + 24h
└─────────────────┘


┌─────────────────┐
│ Agent responds │ ──── Free-form message OK
└─────────────────┘

... 25 hours later ...

┌─────────────────┐
│ n8n sends │
│ reminder │
└─────────────────┘


┌─────────────────┐
│is_in_session_ │ ──── Returns FALSE
│ window() │
└─────────────────┘


┌─────────────────┐
│ Must use │ ──── Template required
│ template │
└─────────────────┘

Twilio Sandbox Setup

For development/testing:

  1. Access Sandbox

    • Twilio Console → Messaging → Try it out → Send a WhatsApp message
  2. Note Sandbox Code

    • Example: join purple-fox
  3. Configure Webhooks

    • When a message comes in: https://norns-pm.ravenhelm.dev/whatsapp/webhook
    • Status callback URL: https://norns-pm.ravenhelm.dev/whatsapp/status
  4. Join from Phone

    • Send join <code> to +1 415 523 8886
  5. Add User Phone to Database

    UPDATE users
    SET phone_number = '+1XXXXXXXXXX'
    WHERE user_id = '<user-id>';

n8n Integration

Daily Briefing Workflow

Trigger (Cron: 7:00 AM)


┌─────────────────┐
│ Get user tasks │ ──── Norns API: /api/tasks
└─────────────────┘


┌─────────────────┐
│ Format briefing │
└─────────────────┘


┌─────────────────┐
│ HTTP Request │ ──── POST /api/whatsapp/send
│ template: │ template_name: "daily_briefing"
│ daily_briefing │ variables: {date, task_count, top_tasks}
└─────────────────┘

Task Reminder Workflow

Trigger (Check every 15 min)


┌─────────────────┐
│ Get tasks due │ ──── Norns API: /api/tasks?due_within=1h
│ within 1 hour │
└─────────────────┘


┌─────────────────┐
│ For each task │
│ HTTP Request │ ──── POST /api/whatsapp/send
│ template: │ template_name: "task_reminder"
│ task_reminder │ variables: {task_name, due_time}
└─────────────────┘

Troubleshooting

Messages Not Received

  1. Check Twilio webhook configuration

    • Console → Messaging → WhatsApp Sandbox → Webhook URL
  2. Verify sandbox membership

    • Send join <code> again from WhatsApp
  3. Check Norns logs

    ssh ravenhelm@100.115.101.81 "docker logs norns-agent 2>&1 | grep -i whatsapp | tail -20"
  4. Verify webhook is accessible

    curl -I https://norns-pm.ravenhelm.dev/whatsapp/webhook

Templates Not Working

  1. Verify ContentSid in environment

    ssh ravenhelm@100.115.101.81 "docker exec norns-agent env | grep WA_TEMPLATE"
  2. Check template exists in Twilio

    twilio api:content:v1:content:list
  3. Verify variables match

    • Template expects positional: {"1": "value", "2": "value"}
    • API expects named: {"task_name": "value", "due_time": "value"}

Session Window Issues

  1. Check session table

    SELECT * FROM whatsapp_sessions WHERE phone_number = '+1234567890';
  2. Check via API

    curl "https://norns-pm.ravenhelm.dev/api/whatsapp/session/+1234567890" \
    -H "X-API-Key: $NORNS_API_KEY"
  3. Verify Redis is running

    ssh ravenhelm@100.115.101.81 "docker exec redis redis-cli ping"

User Not Found

  1. Check phone number format - Must be E.164: +1234567890

  2. Verify user has phone

    SELECT user_id, display_name, phone_number FROM users WHERE phone_number IS NOT NULL;
  3. Add phone to user

    UPDATE users SET phone_number = '+1XXXXXXXXXX' WHERE user_id = '...';

Security Considerations

  1. Webhook Signature Verification

    • All inbound webhooks verify Twilio signature
    • Uses RequestValidator from twilio-python
  2. API Authentication

    • Internal API requires X-API-Key header
    • Key stored in NORNS_API_KEY environment variable
  3. Phone Number Privacy

    • Phone numbers stored in database
    • Only accessible via authenticated API
  4. Template Approval

    • Production templates require Twilio/Meta approval
    • Sandbox templates work immediately for testing
  • [[Norns-Agent|Norns Agent Overview]]
  • [[Norns-Telephony|Norns Telephony (Voice)]]
  • [[Multi-Channel-Sessions|Multi-Channel Session System]]
  • [[Runbooks/WhatsApp-Channel|WhatsApp Runbook]]