Skip to content

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:

/opt/tqwhatsapp/ai-csr-prompt.txt

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

  1. Click New Template
  2. Fill in the required fields: Name, Channel, Provider
  3. For Twilio: enter the Content SID (HXxxx) and template type (MEDIA/CARD)
  4. For Meta: enter the Meta template name (exactly as registered in Business Manager), language code, and component spec JSON
  5. Optionally add variable mapping and button config
  6. 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

  1. Create Content Templates in Twilio Console (Messaging > Content Template Builder)
  2. Content SID format: HXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  3. Template variable mapping: positional {{1}}, {{2}}, etc.
  4. MEDIA type: 4-variable hardcoded mapping + setMediaUrl()
  5. CARD type: configurable variableMapping JSON + buttonConfig JSON
  6. Media URL: CDN prefix applied automatically to relative paths
  7. Template approval: Twilio submits to Meta for approval (24-48 hours)

4. Meta Template Guidelines

  1. Create templates in Meta Business Manager (WhatsApp Manager > Message Templates)
  2. Template name is used instead of Content SID
  3. Component structure: header (image/text), body (text parameters), buttons (URL/quick-reply)
  4. 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.
  5. 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.
  6. Static URL buttons should NOT be included in componentSpec — they have no runtime parameters. The import feature handles this automatically.
  7. lead_notification template: Create a utility template for owner handoff notifications with 3 body parameters: {{1}} product, {{2}} details, {{3}} contact.
  8. Populate componentSpec JSON (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"}
]
  1. language_code must 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.
  2. Variable source identifiers: media_url, recipient_name, message_body, shortlink, button_N_url, static:value
  3. Media URLs must be public HTTPS (stable CDN URL, not presigned S3)
  4. Verify template approval status before use

5. Switching from Twilio to Meta

  1. Prerequisites: Meta Business App, WABA, System User token, MM API ToS accepted
  2. Set broadcast.wa.provider=META in messaging.properties and restart TQPro. New broadcasts will automatically be created with provider=META.
  3. Deploy Python service
  4. Create Meta templates matching existing Twilio templates
  5. Populate tqwa.whatsapp_templates with component_spec for each template
  6. Test: set broadcast.wa.provider=META, run test broadcast with 10-20 contacts
  7. Verify full cycle: dispatch → send → webhook → status update → Java UI
  8. Switch production: broadcast.wa.provider=META, restart TQPro

6. Switching from Meta back to Twilio (Rollback)

  1. Set broadcast.wa.provider=TWILIO in messaging.properties
  2. Restart TQPro — immediately reverts to Twilio dispatch path
  3. Python service can be stopped (no impact on Java)
  4. Note: broadcasts sent via Meta retain wamid.xxx messageSids; new broadcasts get Twilio SMxxx SIDs

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.

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_YEARS controls the retain_until timestamp on conversation_state
  • Conversation messages use the archived flag (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