Skip to content

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

  • 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):

/meta/webhook
/meta/onboarding/*
/twilio/events
/360-dialog/events
/whatsapp/messages/reply

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: PENDINGAPPROVED / 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:

  1. WhatsApp Service calls the Notification Service — agents in the mapped team receive a push notification.
  2. An agent claims the conversation via POST /whatsapp/conversations/{id}/claim (sets claimedByAgentId).
  3. The agent reads history and replies via POST /whatsapp/conversations/{id}/messages.
  4. 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 whatsapp bucket 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 filteringConversationSpec 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 propagationTenantContextFilter populates TenantContextHolder from X-TenantId on every request; public webhook paths bypass this filter.