WhatsApp Service¶
The WhatsApp Service is a Spring Boot microservice that provides a unified WhatsApp Business API abstraction layer across three commercial providers — Meta (direct), Dialog360, and Twilio. It manages outbound campaigns, inbound conversations, message templates, provider onboarding, and AI-assisted conversation routing through Herald (AI path) or human agent notification (human path).
Source
IdeaProjects/NEXIVO/whatsapp-service — Java 21 / Spring Boot 3.4
Tech Stack¶
| Layer | Technology |
|---|---|
| Framework | Spring Boot 3.4.4, Spring Cloud 2024.0.1 |
| Java | 21 |
| Database | PostgreSQL + Spring Data JPA + Flyway |
| Caching | Spring Data Redis |
| Messaging | Spring Kafka |
| HTTP clients | OpenFeign + Spring Cloud LoadBalancer |
| Storage | AWS S3 (v2.17) |
| WhatsApp SDK | Twilio 11 (RC) |
| Phone validation | libphonenumber 8.13 |
| QR codes | Google Zxing 3.5 |
| Observability | Micrometer + Prometheus |
| Multi-tenancy | multi-tenancy-core (internal) |
| Documentation | SpringDoc OpenAPI 2.8 |
Architecture¶
graph TD
subgraph Channels
Meta["Meta Graph API v21.0"]
D360["Dialog360 API"]
Twilio["Twilio API"]
end
subgraph WhatsApp Service
WH["Webhooks\n(Meta / D360 / Twilio)"]
API["REST API\n(12 controllers)"]
SVC["Services\n(Multi-Provider Strategy)"]
KAFKA["Kafka\nwhatsapp-messages"]
SCHED["Schedulers\n(Expiry / Status Poller)"]
end
subgraph Storage
PG["PostgreSQL\n(messages, conversations,\ntemplates, configs)"]
REDIS["Redis\n(cache / sessions)"]
S3["AWS S3\n(media files)"]
end
subgraph Platform
HERALD["Herald\n(Text AI Runtime)"]
CONDUCTOR["Conductor\n(Workflow Engine)"]
CONTACTS["Contact Service"]
end
Meta & D360 & Twilio -->|webhooks| WH
WH --> SVC
API --> SVC
SVC --> KAFKA
KAFKA --> SVC
SVC --> Meta & D360 & Twilio
SVC --> PG & REDIS & S3
SVC --> HERALD & CONDUCTOR & CONTACTS
SCHED --> PG
The active provider is selected by whatsapp.provider (meta | dialog360 | twilio); Spring @ConditionalOnProperty beans load only the active provider's service implementations.
Provider Strategy¶
Meta (Direct — recommended for production)¶
- Communicates with Meta Graph API v21.0.
- Full Embedded Signup OAuth2 onboarding flow.
- Receives delivery receipts and inbound messages via Meta webhooks.
- Template management submitted directly to Meta for approval.
Dialog360¶
- Commercial gateway for Meta.
- Separate API key per config; webhook registered via
POST /360-dialog/link. - Simpler setup, fewer direct Meta credentials needed.
Twilio¶
- Sends via Twilio's messaging API.
- Template status polled every 2 minutes (Twilio doesn't push template events).
- Delivery status received via
POST /twilio/events(Twilio status callback).
Multi-Tenancy¶
Every request carries an X-TenantId header. TenantContextFilter stores this in TenantContextHolder for the duration of the request so all DB queries are automatically scoped. The following paths bypass tenant filtering (public/external callbacks):
During Meta OAuth, the tenant is encoded into the state parameter as "{tenantId}#{wabaId}" so the callback can restore the correct context without a session.
REST API¶
Conversations — /whatsapp/conversations¶
| Method | Path | Purpose |
|---|---|---|
POST |
/ |
Create a new conversation |
GET |
/ |
List conversations (paginated, filtered) |
GET |
/{id} |
Get conversation with full message history |
POST |
/{id}/participants |
Add agent/bot participant |
DELETE |
/{id}/participants/{pid} |
Remove participant |
PATCH |
/{id}/status |
Update conversation status |
POST |
/{id}/claim |
Claim conversation for a human agent |
POST |
/{id}/messages |
Send a message within a conversation |
POST |
/{id}/read |
Mark conversation as read |
POST |
/{id}/close |
Close conversation + trigger Conductor workflow |
Conversations support flexible search via JPA Specifications: filter by status, configId, agentId, contactPhone, contactName, or topic.
Messages — /whatsapp/messages¶
| Method | Path | Purpose |
|---|---|---|
POST |
/ |
Send a template message batch |
POST |
/text |
Send a freeform text message |
POST |
/typing |
Send typing indicator to contact |
GET |
/conversation/{phone} |
Get message history for a contact |
Message Templates — /whatsapp/message-templates¶
| Method | Path | Purpose |
|---|---|---|
POST |
/ |
Create template (multipart — body + optional media file) |
GET |
/ |
Search/list templates (filter: status, category, channel, mediaType) |
GET |
/{id} |
Get template by ID |
DELETE |
/{id} |
Delete template (removes from provider too) |
PATCH |
/{id} |
Update template media file |
Template category values: MARKETING · TRANSACTIONAL · OTP
Template status values: PENDING → APPROVED / REJECTED
File uploads are stored in S3 under the whatsapp bucket folder (max 50 MB per file).
Configuration — /whatsapp/config¶
| Method | Path | Purpose |
|---|---|---|
POST |
/ |
Create a WhatsApp provider config |
GET |
/ |
List all configs |
GET |
/{id} |
Get config by ID |
PUT |
/{id} |
Update config |
DELETE |
/{id} |
Delete config |
GET |
/phone-numbers |
Get provisioned phone numbers |
Each config holds provider credentials: phoneNumberId, wabaId, apiKey, appId, appSecret, webhookVerifyToken.
Meta Onboarding — /meta/onboarding¶
| Method | Path | Purpose |
|---|---|---|
GET |
/init |
Return appId for Embedded Signup SDK |
POST |
/manual |
Manual onboarding with existing WABA credentials |
GET |
/callback |
OAuth callback — exchange code, discover WABA, subscribe, save config |
GET |
/{configId}/status |
Onboarding status: NOT_STARTED · PENDING · COMPLETED |
Webhooks¶
| Method | Path | Provider |
|---|---|---|
GET |
/meta/webhook |
Meta webhook verification (hub.challenge) |
POST |
/meta/webhook |
Inbound messages + delivery updates from Meta |
POST |
/twilio/events |
Twilio status callbacks |
POST |
/360-dialog/events |
Dialog360 webhook events |
POST |
/360-dialog/link |
Register webhook URL with Dialog360 |
Atlas Callback — /whatsapp/messages/reply (Meta provider only)¶
Receives AI pipeline events from Atlas/Herald:
| Event | Action |
|---|---|
message |
Forward AI response to WhatsApp contact |
typing |
Send typing indicator |
done |
Process escalation; close conversation; update contact info |
location_request |
Send location request message |
error |
Log error, no further action |
Callback DTO:
class AtlasCallbackDTO {
String event; // message | typing | done | location_request | error
String turnId;
String conversationId;
String text;
String state;
boolean isFinal;
Map<String, Object> contactUpdates;
String summary;
boolean escalate;
String escalateReason;
boolean resolved;
long latencyMs;
}
Other Endpoints¶
| Path | Purpose |
|---|---|
POST /whatsapp/otp/send |
Send OTP via WhatsApp template |
POST /whatsapp/agent-mappings |
Create agent-to-phone-number mapping |
GET /whatsapp/agent-mappings/ai-agent/{id} |
Mappings for a given AI agent |
GET /whatsapp/agent-mappings/team/{teamId} |
Mappings for a given team |
GET /opt-in/qr |
Generate QR code for WhatsApp opt-in URL |
GET /dashboard/ |
Message counts by status |
GET /dashboard/campaigns |
Message counts per campaign |
Domain Model¶
WhatsAppMessageEntity¶
| Field | Notes |
|---|---|
phoneNumber |
Contact phone number |
messageId |
External provider message ID |
messageStatus |
PENDING · SENT · DELIVERED · READ · RECEIVED · FAILED |
campaignId |
Campaign tracking reference |
configId |
FK to WhatAppConfigEntity |
senderType |
AGENT · CONTACT · SYSTEM |
sentTimestamp |
When sent |
deliveredTimestamp |
When delivered |
readTimestamp |
When read |
contextMessageId |
Parent message ID (for threaded replies) |
ConversationEntity¶
| Field | Notes |
|---|---|
status |
ACTIVE · CLOSED |
agentType |
HUMAN · AI · BOT |
contactPhone |
Stored without leading + |
lastActivityAt |
Used by ConversationExpiryJob |
escalationPending |
Set by Atlas when escalation is required |
heraldContactUpdates |
JSON blob — contact updates from Atlas callback |
conversationSummary |
AI-generated conversation summary |
claimedByAgentId |
Human agent who claimed the conversation |
WhatAppConfigEntity¶
Holds provider credentials per tenant per phone number: phoneNumberId, wabaId, apiKey, appId/Secret, webhookVerifyToken, tenantDefault flag.
WhatsAppTemplateEntity¶
Stores templates with full Meta component JSON (components field), variable mappings (variables), and media URLs (mediaUrls) as TEXT columns. Template status is synced back from the provider after submission.
AgentPhoneMappingEntity¶
Maps a phoneNumberId to either an aiAgentId or a teamId. This is how the service routes inbound messages to the correct AI agent in Atlas or to a human team.
Conversation Flow¶
Routing decision¶
When a webhook arrives, WhatsApp Service looks up the AgentPhoneMappingEntity for the inbound phoneNumberId. The mapping points to either an aiAgentId (route to Herald) or a teamId (notify human agents).
flowchart TD
META[Meta Webhook] --> WAS[WhatsApp Service]
WAS --> LOOKUP{AgentPhoneMapping\nlookup}
LOOKUP -->|aiAgentId| HERALD[Herald\nText AI Runtime]
LOOKUP -->|teamId| NOTIFY[Notification Service]
HERALD -->|callbacks: typing / message / done| WAS
WAS -->|send reply| META2[Meta API]
NOTIFY -->|push notification| AD[Agent Desktop]
AD -->|agent replies| WAS
WAS -->|send reply| META2
HERALD -->|escalate=true| NOTIFY
AI-handled path (Herald)¶
sequenceDiagram
participant Contact
participant Meta
participant WAS as WhatsApp Service
participant Herald
Contact->>Meta: WhatsApp message
Meta->>WAS: POST /meta/webhook
WAS->>WAS: Store message (RECEIVED)
WAS->>WAS: Lookup AgentPhoneMapping → aiAgentId
WAS->>Herald: POST /turn (message + context)
Herald-->>WAS: POST /whatsapp/messages/reply (event=typing)
WAS->>Meta: Send typing indicator
Herald-->>WAS: POST /whatsapp/messages/reply (event=message)
WAS->>Meta: Send AI reply
Meta->>Contact: WhatsApp reply
Herald-->>WAS: POST /whatsapp/messages/reply (event=done)
WAS->>WAS: Update conversation summary + contact info
Human agent path¶
When AgentPhoneMapping points to a teamId, or when Herald sends escalate=true in a done event, the conversation is routed to a human:
- WhatsApp Service calls the Notification Service — agents in the mapped team receive a push notification.
- An agent claims the conversation via
POST /whatsapp/conversations/{id}/claim(setsclaimedByAgentId). - The agent reads history and replies via
POST /whatsapp/conversations/{id}/messages. - WhatsApp Service forwards the reply to Meta and the contact sees it as a normal WhatsApp message.
sequenceDiagram
participant WAS as WhatsApp Service
participant Notify as Notification Service
participant AD as Agent Desktop
participant Meta
participant Contact
WAS->>Notify: Notify team (teamId)
Notify->>AD: Push notification — new conversation
AD->>WAS: POST /whatsapp/conversations/{id}/claim
AD->>WAS: POST /whatsapp/conversations/{id}/messages (reply text)
WAS->>Meta: Send message via provider API
Meta->>Contact: WhatsApp reply
Outbound campaign message¶
sequenceDiagram
Campaign Service->>WhatsApp Service: POST /whatsapp/messages (batch)
WhatsApp Service->>Kafka: Publish to whatsapp-messages topic
Kafka Consumer->>Provider API: Send template message
Provider->>WhatsApp Service: Webhook delivery status
WhatsApp Service->>DB: Update message status
Messaging (Kafka)¶
| Topic | Direction | Message type | Status |
|---|---|---|---|
whatsapp-messages |
Producer | WhatsAppMessage (contact, templateName, campaignId, configId, variables) |
Infrastructure in place; consumer commented out in current codebase |
WhatsappMessagesHandler implements the batch consumer using JSON deserialization and ACK mode. When re-enabled, it calls WhatsAppMessageService.sendMessage() per queued item.
External Integrations¶
Meta Graph API v21.0¶
- Embedded Signup OAuth2 (
/meta/onboarding/callback) - WABA discovery:
GET /{wabaId}/phone_numbers - App subscription:
POST /{wabaId}/subscribe_app - Send message:
POST /{phoneNumberId}/messages - Template management
Atlas / Herald¶
- Inbound messages forwarded to Atlas for AI processing
- Atlas replies received at
/whatsapp/messages/reply - Auth:
Authorization: Bearer ${ATLAS_CALLBACK_SHARED_SECRET}
Conductor (Workflow Engine)¶
- Called via Feign client on conversation close:
POST /api/workflow/{name} - Triggers post-conversation workflows (follow-up, CRM update, etc.)
Contact Service¶
- Feign client lookup:
findContactById() - Contact updates from Atlas applied via
updateContact()/updateContactLanguages()
AWS S3¶
- Template media files (images, PDFs) stored under the
whatsappbucket folder - Pre-signed URL generation for client access
Scheduled Jobs¶
| Job | Schedule | Action |
|---|---|---|
ConversationExpiryJob |
Spring @Scheduled |
Closes ACTIVE conversations with no activity past the configured timeout |
TwilioTemplateStatusPoller |
Every 2 min (${SCHEDULER_DELAY:120000}) |
Polls Twilio template API for status changes and syncs to DB |
Configuration Reference¶
| Property | Env Variable | Default | Purpose |
|---|---|---|---|
whatsapp.provider |
WHATSAPP_PROVIDER |
twilio |
Active provider: meta · dialog360 · twilio |
spring.datasource.url |
POSTGRES_HOST / POSTGRES_PASSWORD |
— | PostgreSQL connection |
spring.datasource.hikari.maximum-pool-size |
DB_MAX_POOL_SIZE |
30 | Connection pool size |
storage.provider |
STORAGE_SERVICE |
s3 |
Object storage backend |
storage.region |
STORAGE_REGION |
eu-west-2 |
S3 region |
storage.endpoint |
STORAGE_ENDPOINT |
— | S3-compatible endpoint URL |
storage.bucketFolder |
STORAGE_BUCKET_FOLDER |
whatsapp |
S3 key prefix |
meta.graph-api-url |
META_GRAPH_API_URL |
https://graph.facebook.com/v21.0 |
Meta Graph API URL |
meta.app-id |
META_APP_ID |
— | Meta application ID |
meta.app-secret |
META_APP_SECRET |
— | Meta application secret |
meta.onboarding-callback-url |
META_ONBOARDING_CALLBACK_URL |
— | OAuth redirect URI |
meta.embedded-signup-config-id |
META_EMBEDDED_SIGNUP_CONFIG_ID |
— | Embedded Signup flow config |
atlas.url |
ATLAS_URL |
— | Atlas/Herald base URL |
atlas.callback-shared-secret |
ATLAS_CALLBACK_SHARED_SECRET |
— | Shared secret for Atlas callbacks |
atlas.connect-timeout-ms |
ATLAS_CONNECT_TIMEOUT_MS |
5000 | Atlas HTTP connect timeout |
atlas.read-timeout-ms |
ATLAS_READ_TIMEOUT_MS |
10000 | Atlas HTTP read timeout |
twilio.account-sid |
TWILIO_ACCOUNT_SID |
— | Twilio account SID |
twilio.auth-token |
TWILIO_AUTH_TOKEN |
— | Twilio auth token |
D360.base-url |
D360_BASE_URL |
https://whatsapp-api |
Dialog360 base URL |
Database¶
Flyway manages schema migrations (classpath:db/migration). Key tables:
| Table | Purpose |
|---|---|
whatsapp_messages_table |
Full message log with delivery timestamps |
whatsapp_conversations |
Conversation state, claims, escalation flags |
whatsapp_conversation_participants |
Agents/contacts in each conversation |
whatsapp_templates_table |
Template definitions with Meta component JSON |
whatsapp_config_table |
Provider credentials per tenant |
whatsapp_agent_phone_mappings |
Phone number → AI agent / team routing table |
Redis
Redis is consumed indirectly via communication-core's ContactService, which uses Spring @Cacheable on findContactById. If Redis requires a password, ensure the correct credentials are set in the Kubernetes secret — a missing or wrong password causes NOAUTH errors at startup. See also the social channel troubleshooting note.
Key Design Patterns¶
Multi-provider strategy — @ConditionalOnProperty ensures only the active provider's beans are loaded. Services (message, template, dashboard) each have a Meta*, Dialog360*, and Twilio* implementation.
Dynamic conversation filtering — ConversationSpec builds JPA Specification objects at runtime for flexible multi-field search without combinatorial query methods.
OAuth state encoding — Meta onboarding encodes tenantId#wabaId into the OAuth state parameter so the callback endpoint can restore tenant context without server-side session storage.
Tenant context propagation — TenantContextFilter populates TenantContextHolder from X-TenantId on every request; public webhook paths bypass this filter.