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:
| Method | Description |
|---|---|
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 Type | Use Case | Limits |
|---|---|---|
| TEXT | Simple responses | 4096 chars |
| QUICK_REPLY | Yes/No, confirmations | Max 3 buttons, 20 chars each |
| LIST | Task lists, menus | Max 10 sections, 10 rows each |
| MEDIA | Images, documents | Varies by type |
| TEMPLATE | Proactive messages | Pre-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)
| Endpoint | Method | Description |
|---|---|---|
/whatsapp/webhook | POST | Receive inbound WhatsApp messages |
/whatsapp/status | POST | Receive delivery status callbacks |
Webhook Payload Fields:
| Field | Description |
|---|---|
From | Sender phone (whatsapp:+1234567890) |
To | Your Twilio number |
Body | Message text |
MessageSid | Unique message ID |
ButtonText | Quick reply button text (if tapped) |
ButtonPayload | Custom payload from button |
NumMedia | Number of attachments |
MediaUrl0 | First media URL |
Internal API (Authenticated)
| Endpoint | Method | Description |
|---|---|---|
/api/whatsapp/send | POST | Send proactive template message |
/api/whatsapp/templates | GET | List configured templates |
/api/whatsapp/session/{phone} | GET | Check 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
| Template | SID | Variables | Message |
|---|---|---|---|
task_reminder | HX685da4c35863ed1180fb46bb8e99004b | task_name, due_time | "Reminder: {{1}} is due {{2}}. Reply to take action." |
daily_briefing | HX47ad70041318b7e3572e586c04a1b4a8 | date, task_count, top_tasks | "Good morning! Here's your briefing for {{1}}:..." |
calendar_reminder | HX657879ccc90ffda83f6648d809febdea | event_name, time, location | "Upcoming: {{1}} at {{2}}\nLocation: {{3}}" |
task_complete_confirm | HX1cf5d904e8e55f6adeb4e59cc9efada1 | task_name | "Task completed: {{1}}" |
weekly_review | HX6680d9a8507e9fe8e7a67e1a83c53a5e | completed_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:
- Window Opens: When user sends an inbound message
- Window Duration: 24 hours from last inbound message
- Inside Window: Can send free-form messages
- 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:
-
Access Sandbox
- Twilio Console → Messaging → Try it out → Send a WhatsApp message
-
Note Sandbox Code
- Example:
join purple-fox
- Example:
-
Configure Webhooks
- When a message comes in:
https://norns-pm.ravenhelm.dev/whatsapp/webhook - Status callback URL:
https://norns-pm.ravenhelm.dev/whatsapp/status
- When a message comes in:
-
Join from Phone
- Send
join <code>to+1 415 523 8886
- Send
-
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
-
Check Twilio webhook configuration
- Console → Messaging → WhatsApp Sandbox → Webhook URL
-
Verify sandbox membership
- Send
join <code>again from WhatsApp
- Send
-
Check Norns logs
ssh ravenhelm@100.115.101.81 "docker logs norns-agent 2>&1 | grep -i whatsapp | tail -20" -
Verify webhook is accessible
curl -I https://norns-pm.ravenhelm.dev/whatsapp/webhook
Templates Not Working
-
Verify ContentSid in environment
ssh ravenhelm@100.115.101.81 "docker exec norns-agent env | grep WA_TEMPLATE" -
Check template exists in Twilio
twilio api:content:v1:content:list -
Verify variables match
- Template expects positional:
{"1": "value", "2": "value"} - API expects named:
{"task_name": "value", "due_time": "value"}
- Template expects positional:
Session Window Issues
-
Check session table
SELECT * FROM whatsapp_sessions WHERE phone_number = '+1234567890'; -
Check via API
curl "https://norns-pm.ravenhelm.dev/api/whatsapp/session/+1234567890" \
-H "X-API-Key: $NORNS_API_KEY" -
Verify Redis is running
ssh ravenhelm@100.115.101.81 "docker exec redis redis-cli ping"
User Not Found
-
Check phone number format - Must be E.164:
+1234567890 -
Verify user has phone
SELECT user_id, display_name, phone_number FROM users WHERE phone_number IS NOT NULL; -
Add phone to user
UPDATE users SET phone_number = '+1XXXXXXXXXX' WHERE user_id = '...';
Security Considerations
-
Webhook Signature Verification
- All inbound webhooks verify Twilio signature
- Uses
RequestValidatorfrom twilio-python
-
API Authentication
- Internal API requires
X-API-Keyheader - Key stored in
NORNS_API_KEYenvironment variable
- Internal API requires
-
Phone Number Privacy
- Phone numbers stored in database
- Only accessible via authenticated API
-
Template Approval
- Production templates require Twilio/Meta approval
- Sandbox templates work immediately for testing
Related Documentation
- [[Norns-Agent|Norns Agent Overview]]
- [[Norns-Telephony|Norns Telephony (Voice)]]
- [[Multi-Channel-Sessions|Multi-Channel Session System]]
- [[Runbooks/WhatsApp-Channel|WhatsApp Runbook]]