Cloudstation

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 user

The bot exposes two HTTP endpoints:

EndpointCalled byPurpose
POST /webhookEvolution APIReceives inbound WhatsApp events.
POST /cloudstation-webhookCloudStationReceives assistant responses.
GET /healthCloudStation or monitoringHealth 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:

  1. A running Evolution API instance.
  2. A WhatsApp number connected to that instance.
  3. The Evolution API base URL.
  4. The Evolution API key.
  5. 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
VariableRequiredDescription
EVOLUTION_API_URLYesBase URL of your Evolution API server.
EVOLUTION_API_KEYYesEvolution API global API key.
EVOLUTION_INSTANCEYesEvolution instance name connected to WhatsApp.
WEBHOOK_PORTNoHTTP port for the bot. Defaults to 3000.
WEBHOOK_BASE_URLYes for hosted callbacksPublic URL where both Evolution API and CloudStation can reach the bot.
CLOUDSTATION_API_URLYesAgents API base URL, usually https://charlie-back.cloud-station.io.
CLOUDSTATION_PROJECT_IDYesTarget project ID.
CLOUDSTATION_API_KEYYes in productionAPI key for the project.
CLOUDSTATION_CLIENT_IDYes in productionClient ID paired with the Agents API key.
CLOUDSTATION_AGENT_TEMPLATE_IDNoSpecific agent template UUID, name, or display name.
CLOUDSTATION_WEBHOOK_SECRETRecommendedSecret used to verify CloudStation callback signatures.
ESCALATION_REMOTE_JIDNoWhatsApp 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 .env

Fill in Evolution API and CloudStation values.

Run locally

npm run dev

Configure 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]:3A4B5C6D

Evolution 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]:3A4B5C6D

Use this for logs, dedupe, and tracing.

Supported Message Types

WhatsApp / Evolution message typeAgents API behavior
conversation / extendedTextMessageForwarded as text.
imageMessageMedia fetched from Evolution API, sent as image content block.
documentMessageMedia fetched and sent as document content block with filename.
audioMessage / voice noteMedia fetched and sent as a document content block with its MIME type.
videoMessageMedia fetched and sent as a document content block when 20 MB or smaller.
stickerMessageSent as text such as "User sent a sticker".
locationMessageSent 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-webhook

Minimal 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-ID

Signature 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:

  1. Trim empty responses.
  2. Split on newline first.
  3. Split on spaces if needed.
  4. Hard-cut only as a last resort.
  5. 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:

  1. Remove the marker from the user-facing message.
  2. Send the clean response to the user.
  3. Notify ESCALATION_REMOTE_JID with 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.com

Update Evolution API webhook

Point Evolution API to the deployed service:

https://your-bot.example.com/webhook

Start the bot

npm start

Test from WhatsApp

Send messages and media to the connected WhatsApp number.

Troubleshooting

SymptomLikely causeFix
Evolution webhook not receivedWrong Evolution webhook URLRe-run /webhook/set/{instance} with your public /webhook URL.
Agents API returns 401Bad CloudStation credentialsCheck CLOUDSTATION_API_KEY and CLOUDSTATION_CLIENT_ID.
Agents API accepts but no WhatsApp replyWEBHOOK_BASE_URL unreachable by CloudStationUse a public HTTPS URL or deploy the bot.
Callback signature failsRaw body not preservedCapture raw body before JSON parsing.
Duplicate WhatsApp repliesCallback retried, or run.completed sent as a replyDedupe by delivery_id and skip run.completed.
Media not processedEvolution media fetch failed or file too largeCheck Evolution logs and send text-only for large files.
Replies go to the wrong userBad JID mappingUse whatsapp-{remoteJid} consistently.
Evolution sends multiple updatesEvent duplicationUse external_message_id and Idempotency-Key.

Next Steps