website/docs/user-guide/messaging/whatsapp-cloud.md
Hermes can connect to WhatsApp through Meta's official WhatsApp Business Cloud API. This is the production-grade path: no Node.js bridge subprocess, no QR codes, no account-ban risk.
In exchange:
If those constraints don't work for your use case, the Baileys bridge integration is the alternative — personal account, no public URL needed, but unofficial and ban-prone.
:::tip Which one should I use?
hermes whatsapp-cloud
The wizard walks you through every credential, validates each one as you paste it (catches the #1 setup trap — pasting a phone number into the Phone Number ID field), and prints exact follow-up instructions for the parts that need to happen outside the wizard (starting cloudflared, configuring Meta's webhook dashboard).
The rest of this page is the manual reference.
cloudflared) is recommended — free, no port forwarding, no domain required. ngrok, your own domain with a reverse proxy + TLS, or a VPS with the gateway directly bound to a public IP all work too.PATH so outbound voice messages render as native WhatsApp voice-note bubbles (green waveform) instead of MP3 audio attachments. Hermes degrades gracefully if absent.You'll need these values from the dashboard — the wizard prompts for them in this order:
| Value | Where in dashboard | Field shape | Notes |
|---|---|---|---|
| Phone Number ID | App Dashboard → WhatsApp → API Setup → below the "From" dropdown | Numeric, 15-17 digits | NOT the phone number itself. The #1 setup mistake is pasting the actual phone number here. |
| Access Token | App Dashboard → WhatsApp → API Setup → "Generate access token" | Starts with EAA, 100+ chars | Temp tokens last 24h — see "Permanent token" below for production. |
| App Secret | App Dashboard → Settings → Basic → click "Show" next to App secret | 32-character lowercase hex | Used to verify incoming webhook signatures. Without it, inbound delivery is refused with 503. |
| App ID (optional) | App Dashboard → Settings → Basic | Numeric, 15-16 digits | Not required for messaging, useful for analytics. |
| WABA ID (optional) | App Dashboard → WhatsApp → API Setup → near the top | Numeric, 15+ digits | Not required for messaging, useful for analytics. |
Temporary access tokens expire after 24 hours, which means a token generated today stops working tomorrow. For production deployments use a System User permanent token:
hermes-bot) → role: Admin.business_managementwhatsapp_business_messagingwhatsapp_business_managementWHATSAPP_CLOUD_ACCESS_TOKEN in ~/.hermes/.env → restart the gateway.System User tokens don't expire unless you explicitly revoke them.
The Cloud API delivers inbound messages by HTTPS POST to your webhook URL — that means the Hermes gateway has to be reachable from Meta's servers. Three common ways:
Free, no port forwarding, works on Windows / macOS / Linux. Runs as a separate process alongside the gateway.
Install:
# Windows
winget install Cloudflare.cloudflared
# macOS
brew install cloudflared
# Linux
# Download the binary from https://github.com/cloudflare/cloudflared/releases
Run a quick tunnel (no Cloudflare account needed — gives you a https://<random>.trycloudflare.com URL):
cloudflared tunnel --url http://localhost:8090
Note the printed URL — that's what you'll give Meta.
:::warning Quick tunnels rotate
The free quick-tunnel URL changes every time you restart cloudflared. For a stable URL, log in with cloudflared tunnel login and create a named tunnel. Free Cloudflare accounts get unlimited named tunnels — see Cloudflare's docs for the named-tunnel workflow.
:::
ngrok http 8090
Free tier shows a different URL on each restart. Paid tier gives you a stable subdomain.
If you already have a server with a TLS cert (Caddy, nginx, etc.), point a route at localhost:8090. This is the most stable option for production but requires existing infrastructure.
Once your tunnel is running:
https://abc123.trycloudflare.com.secrets.token_urlsafe(32); if you're configuring manually, run:
python -c "import secrets; print(secrets.token_urlsafe(32))"
WHATSAPP_CLOUD_VERIFY_TOKEN in ~/.hermes/.env.hermes gateway.https://abc123.trycloudflare.com/whatsapp/webhookTo verify the loop manually (from a third terminal):
TUNNEL="https://abc123.trycloudflare.com"
VERIFY="<your verify token>"
# Should print HTTP 200 with body "hello"
curl -i "$TUNNEL/whatsapp/webhook?hub.mode=subscribe&hub.verify_token=$VERIFY&hub.challenge=hello"
# Health endpoint — should show verify_token_configured: true and app_secret_configured: true
curl "$TUNNEL/health"
In development mode (before your app goes through App Review), Meta restricts which numbers your bot can message:
Up to 5 numbers in dev mode. Going to App Review removes this limit.
In addition to Meta's recipient whitelist, Hermes has its own per-platform allowlist that controls which incoming messages the agent processes. Add to ~/.hermes/.env:
# Comma-separated phone numbers, country code, no '+' / spaces / dashes
WHATSAPP_CLOUD_ALLOWED_USERS=15551234567,15557654321
# Or allow everyone (only safe in combination with Meta's recipient whitelist)
# WHATSAPP_CLOUD_ALLOW_ALL_USERS=true
The wizard sets this in step 6. Without an allowlist, every inbound message is denied — this is intentional, so the bot can't be invoked by random numbers if the recipient whitelist is ever loosened.
WhatsApp displays a name and profile picture for your bot in the chat header and contact list. These can't be set via the Cloud API — they live in Meta's Business Manager.
Once your bot is working, head to business.facebook.com/wa/manage/phone-numbers, click your phone number, and you'll find:
| What | Where | Notes |
|---|---|---|
| Display name | Top of the phone-number page | Changes go through Meta's name-review process (~24–48 hours). |
| Profile picture | Top of the phone-number page | Square image, ≥640×640px recommended. Updates immediately. |
| About / description / website / email / hours / category | "Edit profile" button | These appear in the info pane when a user taps the bot's name. Cosmetic. |
| Verified badge (green checkmark) | Business Manager → Security Center → Start Verification | Requires Meta's separate business verification process. |
The hermes whatsapp-cloud wizard prints these links at the end of setup. None of this is required for the bot to work — it's pure polish for how your bot appears to users.
All settings live in ~/.hermes/.env. Required values are in bold.
| Variable | Default | Description |
|---|---|---|
WHATSAPP_CLOUD_PHONE_NUMBER_ID | — | The 15-17 digit ID from API Setup. Not the phone number. |
WHATSAPP_CLOUD_ACCESS_TOKEN | — | Meta access token (starts with EAA). Temp 24h or System User permanent. |
WHATSAPP_CLOUD_APP_SECRET | — | 32-char hex from Settings → Basic. Without it, inbound is refused with 503. |
WHATSAPP_CLOUD_VERIFY_TOKEN | — | Shared secret for the GET handshake. Auto-generated by the wizard. |
WHATSAPP_CLOUD_ALLOWED_USERS | — | Comma-separated wa_ids allowed to message the bot. |
WHATSAPP_CLOUD_ALLOW_ALL_USERS | false | Set to true to bypass the allowlist. |
WHATSAPP_CLOUD_APP_ID | — | Optional, for future analytics integration. |
WHATSAPP_CLOUD_WABA_ID | — | Optional, for future analytics integration. |
WHATSAPP_CLOUD_WEBHOOK_HOST | 0.0.0.0 | Interface the webhook server binds to. |
WHATSAPP_CLOUD_WEBHOOK_PORT | 8090 | Port the webhook server binds to. Must match the port your tunnel forwards. |
WHATSAPP_CLOUD_WEBHOOK_PATH | /whatsapp/webhook | URL path Meta posts to. |
WHATSAPP_CLOUD_API_VERSION | v20.0 | Meta Graph API version. Only override if a newer version is recommended in Meta's docs. |
WHATSAPP_CLOUD_HOME_CHANNEL | — | wa_id to use as the bot's home channel (for cron jobs etc). |
You can have both the Baileys (whatsapp) and Cloud (whatsapp_cloud) adapters enabled simultaneously, targeting different phone numbers.
.ogg, transcribed via your configured STT provider (local faster-whisper, OpenAI/Nous, Groq, etc.), then handed to the agent as text..txt, .md, .json, .py, .csv, etc.) up to 100KB get inlined into the agent's input so it can read them without a tool call. Larger files are cached locally for the agent's other tools to access.**bold** → *bold*, ~~strike~~ → ~strike~, headers → bold, [link](url) → link (url)). Long messages split at 4096 chars per chunk.When the agent invokes any of these flows, Hermes uses WhatsApp's native interactive messages — tap-to-answer buttons instead of "reply with the number" prompts:
clarify tool — multi-choice questions render as quick-reply buttons (1–3 choices) or a tap-to-open list sheet (4+ choices). Picking "✏️ Other" lets the user type a free-form answer that the agent receives as the resolution.✅ Approve / ❌ Deny buttons instead of needing to type /approve or /deny./reload-mcp show ✅ Approve Once / 🔒 Always / ❌ Cancel buttons.All interactive prompts gracefully degrade to plain text if the buttons fail to render (e.g. on legacy WhatsApp clients).
Hermes acknowledges inbound messages immediately:
This makes it obvious when the bot has seen your message versus when it's still working on a response.
WhatsApp distinguishes between a "voice note" (the green waveform bubble) and a generic audio file attachment. The difference is purely codec: voice notes need to be audio/ogg with opus encoding.
Hermes TTS produces MP3. Two paths:
winget install Gyan.FFmpegbrew install ffmpegYou can check whether the gateway found ffmpeg via the health endpoint:
curl http://localhost:8090/health
# look for "ffmpeg_present": true
Meta only allows free-form messages within a 24-hour window after the user's last inbound message. Outside that window, the only thing Meta's API accepts is a pre-approved message template.
What this means in practice:
131047 ("Re-engagement message").delegate_task async results that take longer than 24h fail the same way.Hermes warns the agent about this window in its system prompt, so the model knows to mention it when scheduling delayed messages.
Message-template support (the workaround for outside-window sends) is not yet implemented in Hermes. If you need it, please open an issue — it's planned but waiting on a clear demand signal.
The Cloud API has limited group support (capability-tier gated by Meta). Hermes's whatsapp_cloud adapter currently handles direct messages only in v1. If you need group chats, use the Baileys bridge.
Meta's default throughput is 80 messages/second per business phone number, with upgrades available. Hermes doesn't currently enforce this client-side — extremely high-volume sends could hit Meta's limit.
Almost always one of:
.env and Meta's dashboard.~/.hermes/.env's WHATSAPP_CLOUD_VERIFY_TOKEN must match exactly what you typed into Meta's dashboard. Run the curl probe above to confirm the gateway's verify handshake works locally first.hermes gateway is up.graph error 100: Object with ID '...' does not existYou pasted your phone number (10-11 digits) into WHATSAPP_CLOUD_PHONE_NUMBER_ID instead of the Phone Number ID (Meta's 15-17 digit internal ID). Re-check the API Setup page — the Phone Number ID is shown below the "From" dropdown.
The wizard catches this with a validator now, but it's worth knowing if you're configuring manually.
graph error 190: Authentication ErrorYour access token is invalid. Subcodes:
subcode 463 — token expired. Temp tokens last 24h. Regenerate, or switch to a System User permanent token (see above).subcode 467 — token invalidated (revoked or password changed).business_management, whatsapp_business_messaging, whatsapp_business_management) were selected.graph error 131047: Re-engagement messageThe 24-hour conversation window expired (see "Known limitations"). Either:
media metadata fetch failed (status=401)Same 401 root causes as outbound (graph error 190) — the access token is invalid or expired. Fix the token.
Common cause: the toolset configured for whatsapp_cloud is missing the tools the agent wants to call. Check hermes tools list and verify the platform is using hermes-whatsapp (the default Cloud adapter toolset, same as Baileys).
If the model emits tool-call-shaped text instead of a structured call, it usually means the toolset was effectively empty. See hermes_cli/platforms.py for the platform → default toolset mapping.
The default stt.provider: local requires pip install faster-whisper. If you're a Nous subscriber, you can route STT through Meta's managed audio gateway instead:
hermes config set stt.provider openai
hermes config set stt.use_gateway true
hermes gateway restart
This uses your Nous Portal access token instead of needing a separate OpenAI key.
WHATSAPP_CLOUD_APP_SECRET is set — leave it set even in development. Without it, the gateway refuses inbound delivery with HTTP 503./health endpoint is unauthenticated — it's safe to expose because it only reports config-presence booleans, not the values themselves. But if you'd rather not surface it, restrict access at the reverse proxy / tunnel layer.Baileys (hermes whatsapp) | Cloud API (hermes whatsapp-cloud) | |
|---|---|---|
| Account type | Personal | Business |
| Setup | QR code scan | Meta app + WABA + token |
| Dependencies | Node.js + npm | Pure Python (httpx + aiohttp) |
| Process | Managed Node subprocess | aiohttp webhook server |
| Public URL needed? | No | Yes |
| Account ban risk | Yes (unofficial API) | No (officially supported) |
| Inbound | Polling Node bridge | Webhook POST from Meta |
| Outbound | Local bridge → Baileys | HTTPS to graph.facebook.com |
| Groups | Full support | DMs only (v1) |
| 24h window | No restriction | Hard rule — templates required after |
| Voice notes (out) | Native | Native with ffmpeg, MP3 fallback otherwise |
| Read receipts | No | Yes (blue double-checkmarks) |
| Typing indicator | No | Yes (auto-dismisses on response) |
| Interactive buttons | Text fallback only | Native (clarify, approval, slash-confirm) |
| Production use | Risky (Meta can ban) | Designed for it |
Most users running Hermes for personal projects prefer Baileys. Most users running customer-facing bots prefer Cloud API.