Evolution API: WhatsApp Integration
Deep dive into Odys's WhatsApp integration via Evolution API, covering its architecture, message templating, deployment on Railway, and the rationale behind its design choices.
Evolution API: WhatsApp Integration
At the heart of Odys's "WhatsApp-first" philosophy lies the Evolution API, a critical component that enables seamless communication between professionals, clients, and the platform's automated systems. This document will guide you through the architecture and implementation of Odys's WhatsApp integration, explaining the why behind its design, how it's deployed, and where we can refine it further. Understanding this integration is key to appreciating how Odys tackles the core problem of appointment management and client engagement in a WhatsApp-centric market.
Overview
Odys leverages the Evolution API to manage all WhatsApp interactions, serving as the single conduit for both automated transactional messages and conversational AI. This strategic choice allows Odys to operate a dedicated corporate WhatsApp number, centralizing all communications without requiring professionals to link their personal phones or manage separate WhatsApp Business accounts. The system is designed to handle outbound messages, route inbound replies intelligently, and provide a consistent user experience through carefully crafted message templates.
The Evolution API instance for local development is defined in docker-compose.evolution.yml, utilizing the atendai/evolution-api:v1.8.2 Docker image. For production, the platform generally refers to Evolution API v2, which is self-hosted on Railway, as detailed in the README.md. This setup ensures that the WhatsApp integration is robust and isolated, running as a dedicated service.
The src/lib/whatsapp.ts Module: The Communication Hub
The src/lib/whatsapp.ts file is the central nervous system for all WhatsApp communications within Odys. Its primary purpose is to abstract away the complexities of the Evolution API, providing a consistent and safe interface for sending messages.
At its core, the module relies on three environment variables: EVOLUTION_API_URL, EVOLUTION_API_KEY, and EVOLUTION_INSTANCE. The isConfigured() function acts as a guard, ensuring that no messages are attempted if these critical settings are missing, preventing runtime errors in misconfigured environments.
Phone Number Normalization
A crucial utility within this module is normalizeBrazilianPhone(). This function addresses a common challenge in WhatsApp integrations: the varied formats of phone numbers. It intelligently converts phone numbers into a standardized international format without the leading +. For Brazilian numbers, it prepends 55 if the country code is missing, while preserving other international numbers. This ensures that messages are always sent to the correct recipient, regardless of how their phone number was initially entered into the system.
Sending Messages with Context
The sendWhatsApp() function is the workhorse for all outbound messages. It takes a phone number (which is first normalized), the text content, and an optional context object. This context is a powerful design choice: when provided, it stores the professionalId and appointmentId in Redis, keyed by the client's phone number. This mechanism is vital for routing client replies. If a client responds to a reminder or confirmation, the incoming message webhook can look up this context to understand which professional or appointment the client is referring to, enabling a more intelligent and personalized AI interaction.
The function also includes basic error handling, logging any failures to connect to the Evolution API or issues with the API response, and gracefully returning false rather than throwing exceptions. This design prioritizes system resilience, ensuring that a WhatsApp delivery failure doesn't cascade into a broader application crash.
Visual Templating for Clarity
Odys employs a clever visual templating system for client-facing transactional messages. Functions like transactionalHeader() and the TRANSACTIONAL_FOOTER constant are used to wrap automated notifications with distinct rule lines and professional names. This visual separation, using HEADER_RULE and TRANSACTIONAL_FOOTER, is critical for user experience. It helps clients quickly distinguish between automated system alerts (like reminders or confirmations) and conversational AI replies within the same WhatsApp chat thread, preventing confusion when the same Odys number handles both channels.
Message Templates
The src/lib/whatsapp.ts module defines 19 distinct message templates, each encapsulated in its own named function (e.g., msgBookingConfirmed, msgReminder24h, msgNewMessageToPro). This approach centralizes all message content, ensuring consistency across the platform and making it easier to manage and update communication flows. These templates are broadly categorized into:
- Client-facing transactional messages: These include confirmations, rejections, cancellations, 24-hour and 1-hour reminders, payment confirmations, and appointment completion notifications. They typically use the
transactionalHeaderandTRANSACTIONAL_FOOTERfor visual disambiguation. - Professional-facing operational messages: These are alerts for new booking requests, client cancellations, payment receipts, and new client messages. These messages are designed to be concise and actionable, delivered directly to the professional's phone from the corporate Odys number.
Evolution API Deployment on Railway
The Evolution API is deployed as a Docker container on Railway, ensuring a dedicated and isolated environment for WhatsApp operations. The docker-compose.evolution.yml file provides the blueprint for this deployment, specifying the atendai/evolution-api:v1.8.2 image.
Key configurations within the Docker setup include:
SERVER_URL: Set tohttp://localhost:8080for local development, indicating the API's public endpoint.AUTHENTICATION_TYPEandAUTHENTICATION_API_KEY: Configured forapikeyauthentication, withodys-local-key-2026as the key for local environments. This secures access to the Evolution API.DATABASE_ENABLEDandREDIS_ENABLED: Both set tofalsein the local Docker setup, indicating that the Evolution API uses local file storage for instance data rather than an external database or Redis. This simplifies local development.WEBHOOK_GLOBAL_ENABLED: Set tofalse. This is a deliberate choice, as webhooks are enabled per instance via an API call (POST {EVOLUTION_API_URL}/webhook/set/{INSTANCE}) after an instance is created. This allows for fine-grained control over webhook behavior, directing inbound messages to Odys'sPOST /api/whatsapp/webhookendpoint.- A Docker volume named
evolution_datais used to persist instance data, ensuring that WhatsApp sessions are maintained across container restarts.
Webhook and Context Routing
The POST /api/whatsapp/webhook API route is the entry point for all inbound WhatsApp messages. When a client sends a message, the Evolution API forwards it to this endpoint. The webhook's intelligence lies in its ability to route these messages effectively. By performing a Redis lookup using last_outbound:{phone}, the system can retrieve the professionalId and appointmentId associated with the client's most recent outbound interaction. This context is then used to seed the AI intake agent (src/lib/ai-intake.ts), allowing the Groq LLM to understand the conversation's history and respond appropriately, even if the client doesn't explicitly mention the professional or appointment in their reply.
Cron Jobs for Automated Communication
Odys relies on two critical cron jobs, defined in vercel.json and exposed as API routes, to manage automated WhatsApp communications:
GET /api/cron/reminders(scheduled daily at0 8 * * *): This endpoint is responsible for sending 24-hour and 1-hour appointment reminders. A clever design choice here is the "conversation-aware delivery" mechanism: reminders are skipped if a client is actively conversing with the AI agent (idle for less than 15 minutes). Deferred reminders are then retried on the next cycle, preventing disruptive interleaving of automated alerts with ongoing dialogues.GET /api/cron/whatsapp-watchdog(scheduled daily at0 9 * * *): This cron job performs a health check on the Evolution API, ensuring its operational status and alerting if there are any issues.
Rate Limiting
To protect the Evolution API from abuse and ensure fair usage, Odys implements a rate limit specifically for WhatsApp interactions. The whatsapp rate limiter, configured for 10 requests per minute with the prefix rl:whatsapp, helps manage the flow of messages and prevents overwhelming the API.
Design Decisions
Why Evolution API?
The decision to use Evolution API was driven by the "WhatsApp-first" strategy and the practical realities of integrating with WhatsApp. Unlike Meta's official WhatsApp Business Cloud API, which can be expensive and has strict template requirements, Evolution API offers a more flexible, self-hosted solution. It acts as a shim over WhatsApp Web, allowing Odys to operate a single, managed corporate WhatsApp number that serves all professionals and clients. This avoids the complexity and cost of requiring each professional to set up their own WhatsApp Business account or link their personal phone, which would be a significant barrier to adoption. The sendWhatsApp() abstraction is a forward-looking design, allowing for a seamless transition to Meta's official API if scale or compliance requirements necessitate it, without rewriting every call site.
Why src/lib/whatsapp.ts as a Central Hub?
Centralizing all WhatsApp message logic in src/lib/whatsapp.ts was a deliberate choice to enforce consistency, improve maintainability, and separate concerns. By defining all 19 message templates as named functions, Odys ensures that every message sent adheres to a consistent tone and structure. This approach also makes it easier to audit and update messages, as all content lives in a single, well-defined location.
Why Visual Templating?
The use of transactionalHeader and TRANSACTIONAL_FOOTER for client-facing messages is a user experience-driven decision. In a scenario where a single WhatsApp thread might contain both automated reminders and conversational AI responses, clear visual cues are essential. These headers and footers disambiguate the message source, helping clients understand whether they are receiving an automated notification or interacting with the AI agent, thereby reducing confusion and improving the overall communication flow.
Why normalizeBrazilianPhone and context?
The normalizeBrazilianPhone function addresses the real-world challenge of inconsistent phone number inputs, ensuring message deliverability. The context parameter in sendWhatsApp and its subsequent Redis lookup for inbound messages is a sophisticated solution to a common problem in multi-entity communication systems. Without it, a client replying to a reminder would have to explicitly state which professional or appointment they are referring to, leading to a cumbersome user experience. By automatically inferring context, Odys makes the AI agent feel more intelligent and the interaction more natural.
Potential Improvements
- Externalize Message Templates for Dynamic Content: Currently, all 19 message templates are hardcoded as functions within
src/lib/whatsapp.ts. While this centralizes content, it requires a code deployment for any text changes. Consider moving these templates to a database or a dedicated content management system. This would allow for dynamic updates, A/B testing of message wording, and easier localization without touching the codebase. For example, themsgBookingConfirmedfunction could fetch its content from a configurable source. - Enhanced Error Handling and Observability for
sendWhatsApp: ThesendWhatsAppfunction currently logs errors to the console and returnsfalseon failure. While functional, this approach could be improved. Implementing a retry mechanism with exponential backoff for transient network issues, or integrating with a dedicated error tracking service (beyond basic console logging) for WhatsApp-specific failures, would increase reliability. Additionally, emitting metrics (e.g., message sent count, failure rate) would provide better observability into the WhatsApp integration's health. - Configuration of Transactional Visual Elements: The
HEADER_RULEandTRANSACTIONAL_FOOTERare hardcoded strings insrc/lib/whatsapp.ts. While effective, making these configurable via environment variables or a settings panel would offer greater flexibility for branding or future design adjustments. Similarly, thedelay: 1200in thesendWhatsAppbody is a magic number that could be externalized as a configuration. - Consistent
APP_URLUsage: TheAPP_URLconstant is imported fromsrc/lib/constants.ts, but in several message templates (e.g.,msgBookingRequest,msgCancelledByProToClient,msgRegistrationInvite), the URL is constructed directly or hardcoded. Ensuring all URL constructions consistently use theAPP_URLconstant would prevent potential issues if the base URL ever changes and improve maintainability.
References
src/lib/whatsapp.tsdocker-compose.evolution.ymlREADME.md
WhatsApp: Corporate Number Architecture & Evolution API
Overview of WhatsApp integration in Odys: corporate number architecture, Evolution API setup, and 19 message templates for transactional flows.
WhatsApp Message Templates
Overview of the 19 message template functions and their formatting strategy in Odys