Marketing Broadcast Feature¶
Overview¶
The Marketing module supports WhatsApp and Email broadcast capabilities, allowing marketing team members to send targeted messages to customer segments. Broadcasts are integrated into the existing campaign/activity workflow and require approval before sending.
WhatsApp broadcasts support two providers: Twilio (legacy, via Content API) and Meta (direct Cloud API via Python service). The provider is configurable per-broadcast or globally.
Activity Types¶
WHATSAPP_BROADCAST— Send WhatsApp messages via Twilio Content API or Meta Cloud APIEMAIL_BROADCAST— Send HTML emails via SMTP with configurable templates
Broadcast Lifecycle¶
Activity created (type = WHATSAPP_BROADCAST or EMAIL_BROADCAST)
↓ (auto-creates broadcast record)
DRAFT → configure template, message body, media
↓ (add one or more Recipient Lists — see recipient-lists.md)
READY → recipients populated (append + dedupe), opt-outs flagged
↓ (activity must be APPROVED first, then trigger send)
SENDING → async send in progress with rate limiting
↓
SENT / FAILED → delivery complete, stats updated via callbacks
Recipient lists are now a first-class entity managed outside the activity (see Recipient Lists). The legacy per-activity CRM-resolve path (broadcast/build-recipients) has been removed; activities populate recipients by copying from one or more recipient lists at compose time.
Provider Branching (WhatsApp)¶
When sendBroadcast() is triggered, the BroadcastDispatcher abstraction routes to the correct provider:
MarketingFacade.sendBroadcast()
↓
BroadcastDispatcherFactory.get(provider)
├── "TWILIO" → TwilioBroadcastDispatcher → BroadcastService (Java, existing)
└── "META" → MetaBroadcastDispatcher → Python /api/broadcast/dispatch
When a WhatsApp broadcast is created, the provider is set from the broadcast.wa.provider config property (default TWILIO). At send time, BroadcastDispatcherFactory reads the stored provider field to select the correct dispatcher.
WhatsApp Provider Selection¶
Twilio (legacy)¶
- Sends via Twilio Content API with Content SID templates
- Status callbacks via Twilio webhooks (
broadcast/webhook/twilio-status) - Rate limiting handled in Java BroadcastService (configurable delay)
Meta (direct Cloud API)¶
- Sends via Python FastAPI service using Meta Marketing Messages API
- Java dispatches to Python via HTTP POST; Python handles the send loop
- Status tracking via Meta webhooks → Python → TQPro internal API
- Template rendering uses
component_specfromtqwa.whatsapp_templatestable - Supports 131049 retry scheduling (per-user marketing throttle)
See doc/features/marketing/whatsapp-meta-integration.md for full Meta integration details.
Database Schema¶
Migrations¶
| Migration | Description |
|---|---|
0058-marketing-broadcast.sql |
Core broadcast tables |
0059-broadcast-media-url.sql |
Media URL support |
0060-broadcast-card-template.sql |
Card template type |
0061-broadcast-templatetype-text-to-media.sql |
Rename TEXT → MEDIA |
0062-broadcast-meta-support.sql |
Widen messagesid to VARCHAR(200), add provider column |
0063-whatsapp-meta-tables.sql |
Create tqwa schema for Python service |
0064-ai-usage-log-csr-support.sql |
AI usage log shared with CSR |
0066-message-template.sql |
Unified mkt_message_template table, templateid FK on broadcast |
0067-backfill-broadcast-templates.sql |
Create templates from existing broadcasts, link by CSID/name |
Tables (Java-owned, nts schema)¶
nts.mkt_message_template— Unified template entity for Twilio and Meta. Stores provider-specific fields (Content SID, Meta name, component_spec), variable mapping, button config, and language code. Referenced by broadcasts via FK.nts.mkt_broadcast— One record per broadcast activity. Stores channel,templateIdFK, message body, media, delivery stats, status, and provider (TWILIO/META). Also retains inline template fields for backward compatibility.nts.mkt_broadcast_recipient— One record per resolved recipient. Tracks customer ID, contact info, message SID (TwilioSMxxxor Metawamid.xxx), delivery status, and error details.nts.mkt_broadcast_optout— Opt-out register keyed by (channel, contactvalue).nts.mkt_recipient_list/nts.mkt_recipient_list_member— System-wide recipient lists and their members (TQ-113, migration 0071). See Recipient Lists.
The legacy nts.mkt_audience and nts.mkt_activityaudience tables are superseded by recipient lists and no longer referenced; scheduled for drop in a follow-up migration.
Tables (Python-owned, tqwa schema)¶
tqwa.whatsapp_templates— Meta templatecomponent_specfor payload construction andlanguage_code(must match the template's language in Meta Business Manager, e.g.,en)tqwa.campaign_messages— WAMID-to-broadcast tracking for webhook correlationtqwa.conversation_state— AI conversation history, lead qualification, routing modetqwa.processed_webhook_messages— Webhook idempotency (deduplicate Meta retries)tqwa.wa_media_events— Media message metadata log (never file content)
Architecture¶
Backend¶
CBroadcast/CBroadcastRecipient/CBroadcastOptout— Canonical entities incom.perun.tlinq.entity.marketingMarketingFacade— Broadcast CRUD, recipient building, send triggering, opt-out managementBroadcastDispatcher— Interface for provider-specific dispatchBroadcastDispatcherFactory— Resolves dispatcher by provider name (TWILIO, META)TwilioBroadcastDispatcher— Delegates toBroadcastService(existing Java path)MetaBroadcastDispatcher— HTTP POST to Python serviceBroadcastService— Singleton async orchestrator for Twilio sends
Send Flow (Twilio)¶
MarketingFacade.sendBroadcast()validates activity is APPROVED and broadcast is READYBroadcastDispatcherFactoryresolves toTwilioBroadcastDispatcher- Delegates to
BroadcastService.executeBroadcast()which runs async - Iterates PENDING recipients with rate limiting (default 75ms)
- Routes to MEDIA or CARD send path, calls
MessageUtil.sendWhatsappMessage()via Twilio - Updates recipient status and broadcast aggregate stats
Send Flow (Meta)¶
MarketingFacade.sendBroadcast()validates as aboveBroadcastDispatcherFactoryresolves toMetaBroadcastDispatcher- HTTP POST to Python
/api/broadcast/dispatch(fire-and-forget) - Python reads dispatch data via TQPro
/internal/broadcast/dispatch-data - Async send loop: resolves variables, builds Meta
components[], sends via Cloud API - Batch status updates via TQPro
/internal/recipient/status - Broadcast stats and status updated via TQPro internal API
Message Template Management¶
Templates are managed as a unified entity (CMessageTemplate / nts.mkt_message_template) supporting both Twilio and Meta providers. The admin UI at template-mgmt.html provides full CRUD management — no more manual CSID entry or SQL inserts.
Template entity fields:
| Field | Description |
|---|---|
name |
Display name |
channel |
WHATSAPP or EMAIL |
provider |
TWILIO or META |
twilioContentSid |
Twilio Content SID (HXxxx) |
metaTemplateName |
Meta template name (as registered in Business Manager) |
languageCode |
Language code for Meta (e.g., en, en_US, ar) |
templateType |
MEDIA, CARD, or TEXT |
variableMapping |
JSON variable mapping |
buttonConfig |
JSON button configuration |
componentSpec |
JSON Meta component_spec for payload construction |
Broadcast → Template link: Broadcasts reference a template by templateId FK. When a template is selected in the broadcast UI, the facade auto-populates the broadcast's inline fields (templateCsid, templateName, templateType, variableMapping, buttonConfig, provider) from the template record. This ensures the Twilio send path (BroadcastService) works unchanged.
Python integration: The dispatch-data internal API response includes the full template entity when templateId is set. The Python sender reads componentSpec and languageCode directly from this response — no separate DB query needed.
WhatsApp Template Types¶
Two template types are supported:
- MEDIA (default/legacy): Hardcoded 4-variable mapping (
{{1}}=media path,{{2}}=name,{{3}}=body,{{4}}=shortlink). Media is attached via Twilio'ssetMediaUrl()parameter. - CARD (
whatsapp/card): Configurable variable mapping stored as JSON. Supports header media + body text + CTA buttons in a single structured message.
Card Template Variable Mapping¶
Each template stores a variableMapping JSON that maps template variable positions to data sources:
Available source identifiers:
| Source | Value |
|---|---|
media_url |
Full CDN URL of broadcast media |
recipient_name |
Recipient's display name |
message_body |
Broadcast message body |
shortlink |
WhatsApp shortlink from config |
button_N_url |
URL suffix from button config at index N |
static:value |
Literal string |
Button Configuration¶
Delivery Tracking¶
- WhatsApp (Twilio): Status callbacks to
broadcast/webhook/twilio-status. Progression: QUEUED → SENT → DELIVERED → READ (or FAILED/UNDELIVERED). - WhatsApp (Meta): Meta webhooks → Python service → TQPro
/internal/recipient/status. Same status progression. WAMID stored inmessagesidcolumn (widened to VARCHAR(200) for Meta format). - Email: SMTP send success sets status to SENT.
Meta Error Handling¶
| Code | Meaning | Action |
|---|---|---|
| 131049 | Per-user marketing throttle | Schedule retry (2h → 12h → 24h) |
| 131050 | User opted out | Add to opt-out register, never retry |
| 131026 | Not on WhatsApp / invalid number | Mark failed, skip |
| 130429 | Throughput exceeded | Back off 5s, reduce rate |
| 131042 | Payment method issue | Alert ops, pause campaign |
AI CSR "Leila"¶
When AI_CSR_ENABLED=true, ALL inbound WhatsApp messages route to the AI agent (AI-first mode). The dedicated WhatsApp number is fully AI-managed — no human operator.
- Persona: Leila, travel specialist at BookMyHoliday
- Product Search: Recognizes staycation/cruise requests, fetches live product data from TQPro APIs, sends options as separate messages with approximate pricing
- Handoff: Two-step flow (collect name + preference), then owner notified via
lead_notificationtemplate - Languages: EN, AR, RU, HI, SR (auto-detected, follows switches, English translations stored)
- Conversation Thread: All messages stored in
tqwa.conversation_messages(encrypted, with WAMID for quote-reply resolution) - Returning Customers: Post-handoff messages route back to AI with previous conversation context
- Offer Context: Broadcast replies include structured package data (hotel, dates, meal plan) in AI system prompt
See doc/features/marketing/whatsapp-meta-integration.md for the full specification.
Dedicated Number (AI-First)¶
The business WhatsApp number is registered to Meta Cloud API only (no WhatsApp Business App). All conversations are AI-managed. The owner is notified of qualified leads via template messages and follows up by calling back on a personal number.
Opt-Out Handling¶
WhatsApp (Twilio)¶
- Custom STOP handler via
broadcast/webhook/twilio-inboundendpoint - Parses STOP/UNSUBSCRIBE/OPTOUT keywords from inbound WhatsApp messages
WhatsApp (Meta)¶
- STOP keyword detection at Priority 2 in Python inbound handler (before AI routing)
- Meta 131050 error auto-adds contact to opt-out register
- Both paths call TQPro
/internal/optout/addorMarketingFacade.addOptout()
Email¶
- RFC 8058
List-Unsubscribeheader in broadcast emails - Links to
broadcast/optout/unsubscribeGET endpoint
API Endpoints¶
All under /tlinq-api/marketing/broadcast/. See doc/api/marketing.md for full specification.
| Endpoint | Auth | Description |
|---|---|---|
broadcast/get |
agent,admin | Get broadcast by activityId |
broadcast/save |
agent,admin | Update broadcast config |
broadcast/compose |
agent,admin | Create/update broadcast and copy recipients from one or more lists (listIds) |
broadcast/send |
admin only | Trigger async send |
broadcast/recipients |
agent,admin | Paginated recipient list |
list/copy-to-broadcast |
agent,admin | Append members from a recipient list to a broadcast (dedupe + opt-out flag). See Recipient Lists. |
broadcast/optout/add |
agent,admin | Add opt-out |
broadcast/optout/remove |
admin only | Remove opt-out |
broadcast/webhook/twilio-status |
public | Twilio delivery status callback |
broadcast/webhook/twilio-inbound |
public | Twilio inbound STOP handler |
broadcast/optout/unsubscribe |
public (GET) | Email unsubscribe |
Removed in TQ-113: broadcast/preview-audience, broadcast/build-recipients, and the marketing/audience/* / marketing/activityaudience/* endpoint groups.
Internal API Endpoints (Python service)¶
| Endpoint | Auth | Description |
|---|---|---|
marketing/internal/broadcast/dispatch-data |
internal | Broadcast + recipients + optouts for dispatch |
marketing/internal/recipient/status |
internal | Batch recipient status updates |
marketing/internal/broadcast/stats |
internal | Recalculate broadcast aggregate stats |
marketing/internal/broadcast/status |
internal | Update broadcast-level status |
marketing/internal/recipient/lookup |
internal | Find recipient by messageSid |
marketing/internal/optout/add |
internal | Add opt-out entry |
marketing/internal/ai/config |
internal | AI configuration (API key, model) |
marketing/internal/ai/usage |
internal | Log AI usage for cost tracking |
Configuration¶
Properties in config/properties.d/messaging.properties:
# Core broadcast settings
broadcast.enabled=true
broadcast.whatsapp.enabled=true
broadcast.email.enabled=true
broadcast.send.delay.ms=75
broadcast.email.send.delay.ms=50
broadcast.phone.prefixes=0,00971,+971
broadcast.phone.default.cc=971
broadcast.max.recipients=10000
# WhatsApp provider: TWILIO (default) or META
broadcast.wa.provider=TWILIO
# Twilio settings
broadcast.wa.template.csid=
broadcast.wa.shortlink=https://wa.me/yourshortlink
broadcast.wa.media.cdn.prefix=https://media.perunapps.com/
broadcast.twilio.status.callback.url=
# Python WhatsApp service (Meta provider)
whatsapp.python.service.url=http://localhost:8001
whatsapp.python.service.api-key=
Email Templates¶
File-based HTML templates in config/email-templates/marketing/. MailUtil placeholder format: %VARIABLE%.
Available placeholders: %SUBJECT%, %BODY%, %RECIPIENT_NAME%, %UNSUBSCRIBE_LINK%.
Frontend¶
The broadcast panel is integrated into the Marketing Planning page (mktplan.html). It appears conditionally when viewing a broadcast activity, showing template configuration, recipient management, delivery statistics, and send controls.
Prerequisites¶
- Twilio path: Register a marketing template with Twilio/Meta. Configure
broadcast.wa.template.csid. - Meta path: Deploy Python service, register templates in Meta Business Manager, populate
tqwa.whatsapp_templateswithcomponent_spec. - Public API URL: Twilio callbacks need
broadcast.twilio.status.callback.url; Meta webhooks need the Python service's public URL registered in Meta App Dashboard. - SMTP Configuration: Email sending requires valid SMTP config.
- WhatsApp Business Verification: Meta-approved marketing templates require a verified WhatsApp Business Account.