WhatsApp Configuration Guide¶
1. TQPro Configuration¶
Broadcast Properties¶
File: config/properties.d/messaging.properties
| Property | Default | Description |
|---|---|---|
broadcast.wa.provider |
TWILIO |
WhatsApp provider: TWILIO or META |
whatsapp.python.service.url |
http://localhost:8001 |
Python service base URL |
whatsapp.python.service.api-key |
(empty) | Shared secret for internal API auth |
broadcast.wa.template.csid |
(empty) | Twilio Content SID for default template |
broadcast.wa.shortlink |
(empty) | WhatsApp shortlink URL |
broadcast.wa.media.cdn.prefix |
(empty) | CDN prefix for media URLs |
broadcast.phone.prefixes |
0,00971,+971 |
Valid phone prefixes |
broadcast.phone.default.cc |
971 |
Default country code for normalization |
broadcast.max.recipients |
10000 |
Maximum recipients per broadcast |
broadcast.send.delay.ms |
75 |
Twilio rate limit (ms between sends) |
broadcast.twilio.status.callback.url |
(empty) | Public URL for Twilio callbacks |
Internal API Authentication¶
The shared secret in whatsapp.python.service.api-key must match the TQPRO_API_KEY in the Python service's .env. This key is used for Bearer token authentication on all /marketing/internal/* endpoints.
The AuthenticationFilter checks this key before JWT/oauth2-proxy validation (Tier 0). Requests matching the key are assigned the internal role.
Claude API Configuration (AI CSR)¶
File: config/tlinqapi.properties
| Property | Default | Description |
|---|---|---|
ai.claude.api-key |
(empty) | Anthropic API key for Claude |
ai.claude.model |
claude-sonnet-4-5-20250929 |
Claude model ID |
ai.claude.max-tokens |
4096 |
Max response tokens per AI call |
System Prompt File¶
The AI CSR system prompt must be deployed alongside the Python service:
Bundled automatically by ./gradlew :tqwhatsapp:deploy from config/ai-csr-prompt.txt. The service logs a warning at startup if missing. Contains: persona definition, recognized product types, product request flow, handoff rules, STATE block format, and translation requirements.
API Roles¶
Configured in config/api-roles.properties:
# Internal API (Python service → Java)
marketing/internal/broadcast/dispatch-data=internal
marketing/internal/recipient/status=internal
marketing/internal/broadcast/stats=internal
marketing/internal/broadcast/status=internal
marketing/internal/recipient/lookup=internal
marketing/internal/optout/add=internal
marketing/internal/ai/config=internal
marketing/internal/ai/usage=internal
# Product APIs (needed by AI CSR product search)
hotel/listPackages=agent,admin,guest,internal
hotel/getPackage=agent,admin,guest,internal
hotel/packageOfferContext=agent,admin,internal
cruise/itinerary/list=guest,agent,admin,internal
cruise/cruise/list=guest,agent,admin,internal
cruise/detail=guest,agent,admin,internal
2. Python Service Configuration (tqwhatsapp)¶
Environment Variables¶
File: tqwhatsapp/.env (git-ignored, never committed). As of multi-tenancy
the .env holds only service-level config — per-tenant Meta credentials,
tenant DB connections, and AI prompts are loaded at runtime from tqplatform
(see the developer guide §Multi-Tenancy). The META_* values here are only the
shared fallback for tenants whose wa_meta_app row has
use_shared_meta_app=true.
| Variable | Required | Description |
|---|---|---|
PLATFORM_DB_HOST |
Yes | tqplatform host (source of the WA registry) |
PLATFORM_DB_PORT |
No | default 5432 |
PLATFORM_DB_NAME |
No | default tqplatform |
PLATFORM_DB_USER |
Yes | platform DB user (e.g. tqpro_platform) |
PLATFORM_DB_PASSWORD |
Yes | platform DB password |
TENANT_DB_HOST |
Yes | tenant DB host (name/user/pass come per-tenant from the registry) |
TENANT_DB_PORT |
No | default 5432 |
META_ACCESS_TOKEN |
Shared-app only | Fallback Meta token for shared-app tenants |
META_APP_SECRET |
Shared-app only | Fallback app secret (HMAC) for shared-app tenants |
META_WABA_ID |
Shared-app only | Fallback WABA id for shared-app tenants |
META_WEBHOOK_VERIFY_TOKEN |
Shared-app only | Fallback verify token for shared-app tenants |
META_WEBHOOK_IP_CHECK |
No | IP check mode: ignore, permit (default), restrict |
META_GRAPH_API_VERSION |
No | Graph API version (default: v25.0) |
TQPRO_API_URL |
Yes | TQPro API base URL (e.g., http://dev-api02:11080/tlinq-api) |
TQPRO_API_KEY |
Yes | Shared secret (must match Java config); also gates /internal/wa/refresh |
TQPRO_ENCRYPTION_KEY |
Yes | 32-byte hex; must equal the Java key. Decrypts per-tenant Meta secrets + tenant DB passwords from tqplatform |
AI_CSR_ENABLED |
No | Default for the per-app flag (wa_meta_app.ai_csr_enabled overrides) |
WA_PROMPTS_DIR |
No | Per-tenant prompt dir (default <deploy-root>/prompts) |
CONVERSATION_ENCRYPTION_KEY |
Yes | AES-256 key, 64 hex chars. Encrypts tqwa message columns. Generate: openssl rand -hex 32 |
DATA_RETENTION_YEARS |
No | Retention for conversation data (default: 5) |
OWNER_EMAIL |
No | Owner email for handoff notifications |
OWNER_WHATSAPP_NUMBER |
No | Owner WhatsApp for handoff alerts (E.164 with + prefix) |
Removed vs. the single-tenant layout: META_PHONE_NUMBER_ID (now per-tenant in
wa_phone_routing) and DB_HOST/DB_NAME/DB_USER/DB_PASSWORD (tenant DB
name/user/pass now come from the registry; only TENANT_DB_HOST/PORT remain).
Per-tenant config is set in tqplatform (not .env): wa_meta_app +
wa_phone_routing rows, plus a tqwa-scoped DB role via
scripts/platform/wa-enable-tenant.sh. Reload the running service with
POST /internal/wa/refresh (bearer TQPRO_API_KEY). See
doc/operations/wa-service-first-deployment.md Part 0.
Startup validation: the service validates TQPRO_API_KEY is set and
CONVERSATION_ENCRYPTION_KEY is a valid 256-bit key; it warns (not fatal) if
PLATFORM_DB_PASSWORD or TQPRO_ENCRYPTION_KEY are missing, since the registry
load and secret decryption depend on them.
Systemd Service¶
# /etc/systemd/system/tqwhatsapp.service
[Unit]
Description=TQPro WhatsApp Service
After=network.target postgresql.service
[Service]
Type=simple
User=tqwhatsapp
WorkingDirectory=/opt/tqwhatsapp
ExecStart=/opt/tqwhatsapp/venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8001 --proxy-headers --forwarded-allow-ips='*'
EnvironmentFile=/opt/tqwhatsapp/.env
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Gradle Deploy¶
# Deploy locally
./gradlew :tqwhatsapp:deploy
# Deploy to remote host
./gradlew :tqwhatsapp:deploy -PdeployHost=wa-server.internal -PdeployUser=deploy
# Local dev
./gradlew :tqwhatsapp:pythonInstall
./gradlew :tqwhatsapp:run
3. Template Management (Admin UI)¶
Templates are managed via the Message Templates admin page (template-mgmt.html), accessible from the Marketing dropdown in the navigation bar.
Creating a Template¶
- Click New Template
- Fill in the required fields: Name, Channel, Provider
- For Twilio: enter the Content SID (
HXxxx) and template type (MEDIA/CARD) - For Meta: enter the Meta template name (exactly as registered in Business Manager), language code, and component spec JSON
- Optionally add variable mapping and button config
- Click Save
Using Templates in Broadcasts¶
When creating a broadcast, select a template from the dropdown instead of manually entering CSIDs. The template's configuration (provider, type, variable mapping, buttons) auto-populates the broadcast record.
Migration from Inline Templates¶
Existing broadcasts that used inline template fields (pre-TQ-111) have been migrated:
- Migration 0066: creates the template table and imports tqwa.whatsapp_templates
- Migration 0067: creates Twilio template records from existing broadcasts and links all broadcasts to their template by CSID or name match
4. Twilio Template Guidelines¶
- Create Content Templates in Twilio Console (Messaging > Content Template Builder)
- Content SID format:
HXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - Template variable mapping: positional
{{1}},{{2}}, etc. - MEDIA type: 4-variable hardcoded mapping +
setMediaUrl() - CARD type: configurable
variableMappingJSON +buttonConfigJSON - Media URL: CDN prefix applied automatically to relative paths
- Template approval: Twilio submits to Meta for approval (24-48 hours)
4. Meta Template Guidelines¶
- Create templates in Meta Business Manager (WhatsApp Manager > Message Templates)
- Template name is used instead of Content SID
- Component structure: header (image/text), body (text parameters), buttons (URL/quick-reply)
- Import from Meta (recommended): In template-mgmt.html, click New Template → Provider = META → Import from Meta. This fetches approved templates from Meta Business Manager and auto-populates componentSpec, variableMapping, and templateType. Review the generated fields before saving.
- Quick Reply buttons (labelled "Custom" in Meta UI) send messages back with the broadcast WAMID as context, triggering AI CSR routing. Use these for marketing templates where AI engagement is desired.
- Static URL buttons should NOT be included in componentSpec — they have no runtime parameters. The import feature handles this automatically.
- lead_notification template: Create a utility template for owner handoff notifications with 3 body parameters:
{{1}}product,{{2}}details,{{3}}contact. - Populate
componentSpecJSON (auto-generated by import, or manually):
[
{"type": "header", "format": "image", "source": "media_url"},
{"type": "body", "parameters": [{"source": "recipient_name"}, {"source": "message_body"}]},
{"type": "button", "sub_type": "url", "index": 0, "source": "button_1_url"}
]
language_codemust exactly match the template's language in Meta Business Manager (e.g.,en,en_US,ar). The Python service reads this from the DB — a mismatch causes silent delivery failure.- Variable source identifiers:
media_url,recipient_name,message_body,shortlink,button_N_url,static:value - Media URLs must be public HTTPS (stable CDN URL, not presigned S3)
- Verify template approval status before use
5. Switching from Twilio to Meta¶
- Prerequisites: Meta Business App, WABA, System User token, MM API ToS accepted
- Set
broadcast.wa.provider=METAinmessaging.propertiesand restart TQPro. New broadcasts will automatically be created with provider=META. - Deploy Python service
- Create Meta templates matching existing Twilio templates
- Populate
tqwa.whatsapp_templateswithcomponent_specfor each template - Test: set
broadcast.wa.provider=META, run test broadcast with 10-20 contacts - Verify full cycle: dispatch → send → webhook → status update → Java UI
- Switch production:
broadcast.wa.provider=META, restart TQPro
6. Switching from Meta back to Twilio (Rollback)¶
- Set
broadcast.wa.provider=TWILIOinmessaging.properties - Restart TQPro — immediately reverts to Twilio dispatch path
- Python service can be stopped (no impact on Java)
- Note: broadcasts sent via Meta retain
wamid.xxxmessageSids; new broadcasts get TwilioSMxxxSIDs
7. AI CSR Operations¶
AI-First Routing¶
When AI_CSR_ENABLED=true, ALL inbound WhatsApp messages route to the AI agent (Leila). Only STOP keywords and echo filter bypass AI. There is no human-default route — the dedicated WhatsApp number is fully AI-managed.
Product Search¶
The AI recognizes product types from the customer's message and fetches live data:
| Product Type | TQPro API | Data Returned |
|---|---|---|
staycation |
hotel/listPackages |
Packages with rooms + pricing |
cruise |
cruise/cruise/list + cruise/detail |
Sailings with cabins + pricing |
other |
N/A | Polite handoff to human |
Products are sent as separate WhatsApp messages with approximate pricing. Adding new product types requires: a Python handler function, a TQPro client method, registration in PRODUCT_HANDLERS, and a line in the system prompt.
Conversation Thread¶
All messages (inbound + outbound) are stored in tqwa.conversation_messages:
- Encrypted original text + encrypted English translation
- WAMID for quote-reply resolution
- archived flag for previous conversation sessions
- Used by Claude for multi-turn history and by future agent review UI
Handoff Flow¶
When the AI qualifies a lead:
1. AI collects customer name + contact preference (call/WhatsApp)
2. AI sends farewell in the customer's language
3. Owner receives lead_notification template with product, details, and contact info
4. Conversation mode switches to human
Returning customers after handoff are routed back to AI with context of the previous conversation. If they reference the same product, the owner is alerted via a return notification.
Data Retention¶
DATA_RETENTION_YEARScontrols theretain_untiltimestamp on conversation_state- Conversation messages use the
archivedflag (not deleted) for session boundaries - GDPR erasure: use
tools/contact_data.py erase --phone +971XXXXXXXXX
Monitoring¶
Key log patterns to watch:
- Fetching staycation products — product search triggered
- Handoff triggered — lead qualification complete
- Quote-reply detected — customer used swipe-reply
- AI response failed — Claude API error, customer gets fallback message
- Return alert sent — returning customer notification to owner