WhatsApp Bot
Build a WhatsApp bot with Evolution API and the CloudStation Agents API.
A WhatsApp bot can use Evolution API for WhatsApp connectivity and the CloudStation Agents API as its AI backend. Evolution API receives WhatsApp messages, your bot forwards them to CloudStation, and CloudStation calls your bot's webhook when the assistant reply is ready.
This pattern is based on the cloudstation-whatsapp-bot boilerplate. The boilerplate supports text, photos, documents, voice notes, videos, stickers, and locations.
Architecture
WhatsApp user
-> WhatsApp
-> Evolution API instance
-> your Node/Express bot /webhook
-> CloudStation Agents API
-> selected CloudStation agent
-> your /cloudstation-webhook callback
-> Evolution API sendText
-> WhatsApp userThe bot exposes two HTTP endpoints:
| Endpoint | Called by | Purpose |
|---|---|---|
POST /webhook | Evolution API | Receives inbound WhatsApp events. |
POST /cloudstation-webhook | CloudStation | Receives assistant responses. |
GET /health | CloudStation or monitoring | Health check. |
How It Works
WhatsApp user sends a message
The user sends a text, image, document, voice note, video, sticker, or location to your connected WhatsApp number.
Evolution API calls your bot
Evolution API POSTs a MESSAGES_UPSERT event to your bot's /webhook endpoint.
Your bot fetches media if needed
For media messages, the bot asks Evolution API for base64 content and MIME type.
Your bot calls the Agents API
The bot sends message, platform: "whatsapp", sender details, media content_blocks, an Idempotency-Key, and response_webhook.
CloudStation replies by callback
CloudStation POSTs the assistant response to /cloudstation-webhook.
Your bot sends the reply to WhatsApp
The bot sends the assistant response back through Evolution API.
Evolution API Prerequisites
You need:
- A running Evolution API instance.
- A WhatsApp number connected to that instance.
- The Evolution API base URL.
- The Evolution API key.
- The Evolution instance name.
Environment Variables
EVOLUTION_API_URL=https://evo.your-domain.com
EVOLUTION_API_KEY=your_evolution_api_key
EVOLUTION_INSTANCE=your_instance_name
WEBHOOK_PORT=3000
WEBHOOK_BASE_URL=https://your-bot.example.com
CLOUDSTATION_API_URL=https://charlie-back.cloud-station.io
CLOUDSTATION_PROJECT_ID=your_project_id
CLOUDSTATION_API_KEY=your_project_api_key
CLOUDSTATION_CLIENT_ID=your_project_client_id
CLOUDSTATION_AGENT_TEMPLATE_ID=optional_agent_template_id_or_name
CLOUDSTATION_WEBHOOK_SECRET=optional_project_webhook_secret
ESCALATION_REMOTE_JID=optional_whatsapp_jid_for_handoff| Variable | Required | Description |
|---|---|---|
EVOLUTION_API_URL | Yes | Base URL of your Evolution API server. |
EVOLUTION_API_KEY | Yes | Evolution API global API key. |
EVOLUTION_INSTANCE | Yes | Evolution instance name connected to WhatsApp. |
WEBHOOK_PORT | No | HTTP port for the bot. Defaults to 3000. |
WEBHOOK_BASE_URL | Yes for hosted callbacks | Public URL where both Evolution API and CloudStation can reach the bot. |
CLOUDSTATION_API_URL | Yes | Agents API base URL, usually https://charlie-back.cloud-station.io. |
CLOUDSTATION_PROJECT_ID | Yes | Target project ID. |
CLOUDSTATION_API_KEY | Yes in production | API key for the project. |
CLOUDSTATION_CLIENT_ID | Yes in production | Client ID paired with the Agents API key. |
CLOUDSTATION_AGENT_TEMPLATE_ID | No | Specific agent template UUID, name, or display name. |
CLOUDSTATION_WEBHOOK_SECRET | Recommended | Secret used to verify CloudStation callback signatures. |
ESCALATION_REMOTE_JID | No | WhatsApp JID to notify for human handoff. |
Legacy CHARLIE_* variable names (CHARLIE_API_URL, CHARLIE_API_KEY, CHARLIE_CLIENT_ID, CHARLIE_PROJECT_ID, CHARLIE_WEBHOOK_SECRET) are still read for backward compatibility.
Keep both Evolution API credentials and CloudStation Agents API credentials server-side. They should never be exposed in a public website or browser bundle.
Quick Start
Configure the bot
Create .env from the boilerplate example.
cp .env.example .envFill in Evolution API and CloudStation values.
Install dependencies
npm installRun locally
npm run devConfigure Evolution API webhook
Tell Evolution API to send inbound WhatsApp events to your bot.
Test WhatsApp messages
Send a message to the connected WhatsApp number and confirm the bot replies.
Configure the Evolution Webhook
Set the Evolution API webhook to your bot's /webhook endpoint.
curl -X POST 'https://evo.your-domain.com/webhook/set/your_instance_name' \
-H 'apikey: your_evolution_api_key' \
-H 'Content-Type: application/json' \
-d '{
"webhook": {
"url": "https://your-bot.example.com/webhook",
"enabled": true,
"byEvents": false,
"events": ["MESSAGES_UPSERT"]
}
}'For local testing, use a tunnel or make sure Evolution API can reach your machine:
{
"webhook": {
"url": "https://your-tunnel.example.com/webhook",
"enabled": true,
"byEvents": false,
"events": ["MESSAGES_UPSERT"]
}
}Agents API Request Built by the Bot
The WhatsApp bot sends Agents API requests like this (the boilerplate posts the same body, plus conversation_id, to the legacy /v1/projects/{project_id}/api/message route):
{
"message": "Hello from WhatsApp",
"platform": "whatsapp",
"response_webhook": "https://your-bot.example.com/cloudstation-webhook",
"agent_template_id": "optional-agent-template",
"sender": {
"id": "[email protected]",
"name": "Jane Customer"
},
"external_message_id": "whatsapp:[email protected]:3A4B5C6D"
}sent with headers including an idempotency key derived from the WhatsApp message ID:
Idempotency-Key: whatsapp:[email protected]:3A4B5C6DEvolution API can redeliver the same inbound event, so using the WhatsApp message ID as Idempotency-Key prevents a redelivered event from triggering a duplicate agent run.
The core client pattern:
const body: Record<string, unknown> = {
message,
platform: 'whatsapp',
response_webhook: `${webhookBaseUrl.replace(/\/+$/, '')}/cloudstation-webhook`,
};
if (agentTemplateId) body.agent_template_id = agentTemplateId;
if (sender) body.sender = sender;
if (contentBlocks.length > 0) body.content_blocks = contentBlocks;
if (externalMessageId) body.external_message_id = externalMessageId;
await fetch(`${apiUrl}/v1/projects/${projectId}/conversations/${conversationId}/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'x-client-id': clientId,
'Idempotency-Key': `whatsapp:${remoteJid}:${messageId}`,
},
body: JSON.stringify(body),
});Conversation IDs for WhatsApp
Use the WhatsApp remote JID as the stable thread key.
Recommended format:
whatsapp-{remoteJid}Example:
This keeps every WhatsApp user or group in a stable CloudStation conversation.
External Message IDs for WhatsApp
Recommended format:
whatsapp:{remoteJid}:{messageId}Example:
whatsapp:[email protected]:3A4B5C6DUse this for logs, dedupe, and tracing.
Supported Message Types
| WhatsApp / Evolution message type | Agents API behavior |
|---|---|
conversation / extendedTextMessage | Forwarded as text. |
imageMessage | Media fetched from Evolution API, sent as image content block. |
documentMessage | Media fetched and sent as document content block with filename. |
audioMessage / voice note | Media fetched and sent as a document content block with its MIME type. |
videoMessage | Media fetched and sent as a document content block when 20 MB or smaller. |
stickerMessage | Sent as text such as "User sent a sticker". |
locationMessage | Sent as text with latitude and longitude. |
Files larger than 20 MB should be sent as text-only descriptions instead of base64 payloads.
Media Example
For an image message:
{
"message": "User sent a photo with caption: What is wrong with this error screen?",
"platform": "whatsapp",
"response_webhook": "https://your-bot.example.com/cloudstation-webhook",
"content_blocks": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": "BASE64_IMAGE_DATA"
}
}
]
}For a document:
{
"type": "document",
"source": {
"type": "base64",
"media_type": "application/pdf",
"data": "BASE64_DOCUMENT_DATA",
"filename": "contract.pdf"
}
}Long-Running Replies
Some agent runs take longer than a user wants to wait on a webhook alone. Use the task_id from the 202 response to poll status directly:
GET /v1/projects/{project_id}/tasks/{task_id}Poll while the task is queued or running, and stop once it reaches completed, failed, canceled, or timed_out. waiting_input means the agent asked the user a question and waits for the next message. See Tasks & Events for the full status table and polling pattern.
Receiving CloudStation Replies
Your bot exposes:
POST /cloudstation-webhookMinimal callback handler:
router.post('/cloudstation-webhook', async (req, res) => {
res.sendStatus(200);
const payload = req.body;
const conversationId = payload?.conversation_id;
const content = payload?.message?.content;
if (!conversationId || !content) return;
if (payload.type === 'run.completed') return; // repeats the last reply
const remoteJid = conversationId.replace(/^whatsapp-/, '');
await evolution.sendText(remoteJid, content);
});Production handlers should verify signatures, dedupe callbacks, split long messages, and handle platform send errors.
Callback Signature Verification
If CLOUDSTATION_WEBHOOK_SECRET is configured, CloudStation includes:
X-CloudStation-Timestamp
X-CloudStation-Signature
X-CloudStation-Delivery-IDSignature formula:
v1=hex(HMAC_SHA256(secret, timestamp + "." + raw_request_body))The WhatsApp boilerplate verifies the signature before sending any WhatsApp reply. See Agents API Webhooks for complete verification code.
Long WhatsApp Replies
WhatsApp supports long messages, but integrations should still handle size limits. The boilerplate splits long assistant responses into chunks before sending.
Recommended behavior:
- Trim empty responses.
- Split on newline first.
- Split on spaces if needed.
- Hard-cut only as a last resort.
- Send chunks in order.
Human Handoff Pattern
The boilerplate supports an escalation marker:
[ESCALATE: short summary of why human help is needed]When present at the end of an assistant response, the bot can:
- Remove the marker from the user-facing message.
- Send the clean response to the user.
- Notify
ESCALATION_REMOTE_JIDwith the summary and conversation details.
Deploy to CloudStation
Deploy the bot service
Deploy the WhatsApp bot as a CloudStation service.
Set production environment variables
Set WEBHOOK_BASE_URL to the service's public URL.
WEBHOOK_BASE_URL=https://your-bot.example.comUpdate Evolution API webhook
Point Evolution API to the deployed service:
https://your-bot.example.com/webhookStart the bot
npm startTest from WhatsApp
Send messages and media to the connected WhatsApp number.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Evolution webhook not received | Wrong Evolution webhook URL | Re-run /webhook/set/{instance} with your public /webhook URL. |
Agents API returns 401 | Bad CloudStation credentials | Check CLOUDSTATION_API_KEY and CLOUDSTATION_CLIENT_ID. |
| Agents API accepts but no WhatsApp reply | WEBHOOK_BASE_URL unreachable by CloudStation | Use a public HTTPS URL or deploy the bot. |
| Callback signature fails | Raw body not preserved | Capture raw body before JSON parsing. |
| Duplicate WhatsApp replies | Callback retried, or run.completed sent as a reply | Dedupe by delivery_id and skip run.completed. |
| Media not processed | Evolution media fetch failed or file too large | Check Evolution logs and send text-only for large files. |
| Replies go to the wrong user | Bad JID mapping | Use whatsapp-{remoteJid} consistently. |
| Evolution sends multiple updates | Event duplication | Use external_message_id and Idempotency-Key. |