TL;DR
- Meta’s Facebook app config allows a single webhook callback URL (per Chatwoot’s WhatsApp FAQ). For a team inbox plus an AI agent, let Chatwoot own the WhatsApp Cloud API channel and use Hermes as the brain behind a Chatwoot Agent Bot webhook — not as a second WhatsApp webhook receiver.
- A practical stack (July 2026, editorial): WhatsApp Cloud API → Chatwoot inbox → Agent Bot webhook → small bridge service → Hermes API server (OpenAI-compatible) with
OPENAI_API_KEY→ reply posted back through Chatwoot → Meta delivers to WhatsApp. - Hermes direct WhatsApp (
hermes whatsapp-cloud) is a reasonable path for a solo bot without a support desk. This article focuses on the Chatwoot + Hermes pattern for support teams that need visibility, assignment, and human takeover. - Go-live gotchas: subscribe your Meta app to the WABA (
subscribed_apps), respect the 24-hour customer service window, use a System User permanent token, and test dev-mode phone number formatting carefully. - Hermes value here: full agent loop (tools, skills, memory, sessions) behind OpenAI — not a thin ChatGPT wrapper in the bridge. The sample bridge uses Hermes’s Responses API
conversationparameter for multi-turn continuity;X-Hermes-Session-Keyscopes long-term memory, not transcript history.
What You Will Learn Here
- Why Chatwoot and Hermes should not both receive WhatsApp webhooks from Meta
- A reference architecture for WhatsApp → Chatwoot → Hermes → OpenAI
- How to set up Chatwoot’s WhatsApp Cloud API inbox (manual flow)
- How to configure Hermes with OpenAI and expose it via the API server
- A minimal FastAPI bridge that connects Chatwoot Agent Bot events to Hermes
- Human handoff, the 24-hour messaging window, and operational checklist
- Common failure modes from Meta and Chatwoot issue threads
This is Part 2 of the Hermes Agent series. Start with Part 1: Hermes architecture and RAG lessons. Part 3 covers scheduled cron ops; Part 4 covers internal support copilots for human agents.
The Architecture Question
You have four ingredients:
| Component | Role |
|---|---|
| WhatsApp Cloud API | Official Meta channel for inbound/outbound WhatsApp messages |
| Chatwoot | Team inbox, conversation history, agent assignment, human handoff |
| Hermes Agent | Self-hosted agent runtime — tools, skills, memory, sessions |
| OpenAI API | LLM provider Hermes calls for reasoning and tool use |
The mistake to avoid: pointing Meta’s webhook at Hermes and Chatwoot. Per Chatwoot’s manual WhatsApp guide, a Facebook app configures only one webhook endpoint — you point it at Chatwoot’s callback URL. Other numbers on the same app can share that endpoint via additional Chatwoot inboxes, but you do not get two independent Meta webhook receivers.
So pick an owner for the WhatsApp webhook:
Option A — Support desk (this article)
WhatsApp → Meta → Chatwoot → Bridge → Hermes (+ OpenAI) → Chatwoot → WhatsApp
Option B — Solo Hermes bot (no Chatwoot)
WhatsApp → Meta → Hermes gateway (hermes whatsapp-cloud) → OpenAI
This article implements Option A.
When to use Option A vs Option B
| Option A — Chatwoot + Hermes (this article) | Option B — Hermes direct | |
|---|---|---|
| Best for | Support teams, human handoff, conversation audit in a team inbox | Solo bots, personal assistants, fastest first message |
| Team visibility | Agents see full WhatsApp history in Chatwoot | No shared inbox; you build your own logging |
| Human takeover | Native via Chatwoot toggle_status → open | You implement escalation yourself |
| Webhook owner | Chatwoot (one Meta callback URL) | Hermes gateway (hermes whatsapp-cloud) |
| Time to first reply | Higher — bridge + inbox + bot setup | Lower — wizard + tunnel |
| Compliance / ops | Conversation audit lives in Chatwoot; Hermes sessions for agent reasoning | You own both audit trails |
Reference Architecture
CUSTOMER PHONE
|
v
+---------------------+
| WhatsApp Cloud API |
| (Meta Graph API) |
+----------+----------+
| HTTPS webhook
v
+---------------------+
| Chatwoot |
| WhatsApp inbox |
| conversations DB |
+----------+----------+
|
message_created webhook (Agent Bot)
v
+---------------------+
| Bridge service |
| (FastAPI) |
| - verify Chatwoot |
| - map conversation |
| - call Hermes API |
+----------+----------+
| OpenAI-compatible HTTP
v
+---------------------+
| Hermes API server |
| (+ AIAgent loop) |
+----------+----------+
|
v
+---------------------+
| OpenAI API |
| (gpt-4o, etc.) |
+---------------------+
Reply path:
Bridge --POST--> Chatwoot Messages API (agent bot token)
--> Chatwoot --> Meta --> customer WhatsApp
Why a bridge service?
Chatwoot’s Agent Bot sends message_created events to your outgoing_url. Hermes exposes an OpenAI-compatible API server — not a native Chatwoot adapter. A thin bridge translates between the two protocols.
The bridge should stay dumb: validate webhooks, extract the user message, call Hermes, post the reply. Keep skills, tools, memory, and session logic inside Hermes.
Apply Part 1 patterns here
Part 1 described Hermes as a harness, not a thin LLM proxy. Carry these patterns into the WhatsApp stack:
| Part 1 pattern | WhatsApp implementation |
|---|---|
| Sessions as infrastructure | Responses API conversation key per Chatwoot conversation (see Step 5) |
| Repo-native instructions | AGENTS.md for support rules, HANDOFF keyword, tone — not hardcoded bridge prompts |
| Tool exposure per surface | Narrow to the WhatsApp toolset in Step 3; avoid full terminal/browser on customer chat |
| Bounded memory + on-demand recall | Let Hermes load MEMORY.md / skills; bridge does not inject policy text |
| Write-approval gates | Optional skills.write_approval if the bot learns support macros from conversations |
If your support bot also needs document grounding, see RAG Apps in Practice for eval gates before enabling qmd/wiki skills on customer-facing channels.
Prerequisites
Before you wire components together:
- Meta Business account and a WhatsApp app with Cloud API enabled
- Dedicated business phone number (not your personal WhatsApp)
- Chatwoot instance — Cloud or self-hosted with a public HTTPS URL
- Hermes Agent installed on a host that can run the gateway/API server
- OpenAI API key with billing enabled
- Public HTTPS for Chatwoot (Meta must reach your webhook). Hermes’s WhatsApp Cloud docs mention Cloudflare Tunnel and ngrok for local dev; Chatwoot itself just needs a URL Meta can reach.
Step 1: Set Up Chatwoot’s WhatsApp Cloud API Inbox
Follow Chatwoot’s manual WhatsApp Cloud API guide:
- Create a permanent System User access token in Meta Business Manager with
whatsapp_business_messagingandwhatsapp_business_managementpermissions. - In Chatwoot: Settings → Inboxes → Add inbox → WhatsApp.
- Enter your phone number, Phone Number ID, and Business Account ID from Meta’s API Setup page.
- Copy Chatwoot’s webhook URL (format:
https://<your-chatwoot>/webhooks/whatsapp/<phone_number>) and verify token. - In Meta Developer Portal → WhatsApp → Configuration, paste the callback URL and verify token. Subscribe to the messages field.
Critical Meta step: subscribe the app to your WABA
Multiple Chatwoot GitHub issues (#11725, #13097) report webhooks verified but no messages arriving. Community reports and Meta support point to subscribing the app to the WABA:
curl -X POST \
"https://graph.facebook.com/v<API_VERSION>/<WABA_ID>/subscribed_apps" \
-H "Authorization: Bearer <ACCESS_TOKEN>"
Replace v<API_VERSION> with the Graph API version shown in your Meta app dashboard (for example v21.0 or newer).
Also confirm under Webhook fields → messages that message delivery is subscribed. Chatwoot merged automation for this in PR #13278 (January 2026), but the manual curl is still worth knowing for debugging.
Dev-mode phone numbers
In development mode, Meta restricts which numbers you can message. Add testers under API Setup → Manage phone number list. If outbound messages fail with error #131030 Recipient phone number not in allowed list, check that the stored phone format matches Meta’s allowed-list format exactly — Chatwoot issue #13932 documents Argentine number formatting mismatches.
Step 2: Configure Hermes with OpenAI
Hermes supports OpenAI through the openai-api provider. Per the AI Providers docs:
# ~/.hermes/.env
OPENAI_API_KEY=sk-...
# Interactive model selection
hermes model
# Choose "OpenAI API (direct)" and pick a model (e.g. gpt-4o)
Or set in ~/.hermes/config.yaml:
model:
provider: openai-api
default: gpt-4o
Verify with a local chat:
hermes chat -q "Reply with exactly: Hermes OpenAI OK"
See Configuring Models for provider-specific env vars and config.yaml layout.
Optional: project context for support
Add repo-specific instructions Hermes will load automatically:
<!-- AGENTS.md in your project root -->
# Support Agent Rules
- You are Acme Corp's WhatsApp support assistant.
- Be concise. WhatsApp messages should be short paragraphs, not essays.
- If the user asks for a human, reply HANDOFF and stop.
- Never invent order IDs. Use the order_lookup tool when available.
Hermes loads AGENTS.md, .hermes.md, and compatible context files per its Prompt Assembly docs.
Step 3: Start the Hermes API Server
The API server exposes Hermes as an OpenAI-compatible HTTP endpoint on port 8642 by default.
# ~/.hermes/.env
API_SERVER_ENABLED=true
API_SERVER_KEY=change-me-to-a-long-random-secret
API_SERVER_HOST=127.0.0.1
API_SERVER_PORT=8642
# Start gateway (API server runs alongside it)
hermes gateway
Health check:
curl -H "Authorization: Bearer $API_SERVER_KEY" \
http://127.0.0.1:8642/health
The API server gives callers access to Hermes’s full toolset. Treat API_SERVER_KEY as a deployment secret — the docs require it even on loopback.
Scope tools for WhatsApp
Customer-facing WhatsApp turns should not expose the full terminal/browser tool surface. Per Hermes’s WhatsApp Cloud troubleshooting docs, the default platform mapping uses the hermes-whatsapp toolset. When starting the gateway for this integration, restrict exposed tools to what support agents actually need:
# Example: start gateway with a WhatsApp-safe tool surface
hermes gateway --toolsets hermes-whatsapp,skills
Verify the exact flag against your Hermes version (hermes gateway --help). For API-server-only calls, Hermes resolves toolsets for the api_server platform — confirm via GET /v1/toolsets with your API_SERVER_KEY.
Step 4: Create the Chatwoot Agent Bot
Create a webhook bot in Chatwoot. Via API (developer docs):
curl -X POST "https://<chatwoot-host>/api/v1/accounts/<account_id>/agent_bots" \
-H "api_access_token: <admin-token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Hermes WhatsApp Agent",
"description": "Hermes + OpenAI support bot",
"outgoing_url": "https://<bridge-host>/chatwoot/webhook",
"bot_type": 0
}'
Save two different credentials from this step:
| Credential | Purpose |
|---|---|
access_token (agent bot) | Bridge uses this as api_access_token to post replies and toggle conversation status |
| Webhook signing secret (if shown) | Separate from the bot token — used to verify X-Chatwoot-Signature on inbound webhooks |
Do not confuse the bot access_token with Meta’s WhatsApp verify token or with Hermes’s API_SERVER_KEY. Per Chatwoot’s webhook verification guide, account-level webhooks sign with sha256=HMAC-SHA256(secret, "{timestamp}.{raw_body}"). Agent Bot outgoing_url webhooks may not include signatures on all Chatwoot versions yet (chatwoot/chatwoot#13809) — implement verification when your instance provides a secret, and restrict bridge ingress by network policy until then.
Connect the bot to your WhatsApp inbox: Inbox Settings → Bot Configuration → select the bot → Save. New conversations will enter pending status and Chatwoot will POST events to your bridge.
Step 5: Build the Bridge Service
Minimal FastAPI bridge. It listens for Chatwoot Agent Bot webhooks, verifies signatures when a signing secret is available, calls Hermes /v1/responses with a stable conversation key for multi-turn continuity, and posts the reply back. The handler only auto-replies to incoming messages (message_type == 0) on pending conversations.
Channel rules (tone, HANDOFF keyword, support macros) belong in AGENTS.md from Step 2 — not hardcoded in the bridge. Hermes layers frontend instructions on top of its core prompt per the API server docs.
# bridge/main.py
import hashlib
import hmac
import json
import os
import time
import httpx
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
CHATWOOT_BASE = os.environ["CHATWOOT_BASE_URL"].rstrip("/")
CHATWOOT_ACCOUNT_ID = os.environ["CHATWOOT_ACCOUNT_ID"]
CHATWOOT_BOT_TOKEN = os.environ["CHATWOOT_BOT_TOKEN"]
CHATWOOT_BOT_WEBHOOK_SECRET = os.environ.get("CHATWOOT_BOT_WEBHOOK_SECRET", "")
HERMES_BASE = os.environ.get("HERMES_API_BASE", "http://127.0.0.1:8642")
HERMES_API_KEY = os.environ["HERMES_API_KEY"]
HERMES_MODEL = os.environ.get("HERMES_MODEL", "hermes-agent")
INCOMING = 0
OUTGOING = 1
def verify_chatwoot(request: Request, body: bytes) -> None:
"""Verify Chatwoot webhook signature when a signing secret is configured."""
if not CHATWOOT_BOT_WEBHOOK_SECRET:
return
timestamp = request.headers.get("X-Chatwoot-Timestamp", "")
received = request.headers.get("X-Chatwoot-Signature", "")
if not timestamp or not received:
raise HTTPException(status_code=401, detail="missing signature headers")
try:
ts = int(timestamp)
except ValueError as exc:
raise HTTPException(status_code=401, detail="invalid timestamp") from exc
if abs(time.time() - ts) > 300:
raise HTTPException(status_code=401, detail="stale webhook timestamp")
signed_payload = f"{ts}.".encode() + body
expected = "sha256=" + hmac.new(
CHATWOOT_BOT_WEBHOOK_SECRET.encode(),
signed_payload,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, received):
raise HTTPException(status_code=401, detail="invalid signature")
def is_incoming_message(message: dict) -> bool:
message_type = message.get("message_type")
return message_type in (INCOMING, "incoming")
def extract_response_text(data: dict) -> str:
for item in data.get("output", []):
if item.get("type") != "message":
continue
for part in item.get("content", []):
if part.get("type") == "output_text":
text = (part.get("text") or "").strip()
if text:
return text
return ""
async def ask_hermes(user_text: str, conversation_key: str) -> str:
# Responses API chains turns via `conversation` — unlike stateless /v1/chat/completions
payload = {
"model": HERMES_MODEL,
"input": user_text,
"conversation": conversation_key,
"store": True,
}
headers = {
"Authorization": f"Bearer {HERMES_API_KEY}",
"Content-Type": "application/json",
# Scopes long-term memory providers (e.g. Honcho) — not transcript replay by itself
"X-Hermes-Session-Key": conversation_key,
}
async with httpx.AsyncClient(timeout=120.0) as client:
resp = await client.post(
f"{HERMES_BASE}/v1/responses",
json=payload,
headers=headers,
)
resp.raise_for_status()
data = resp.json()
return extract_response_text(data)
async def post_chatwoot_reply(conversation_id: int, content: str) -> None:
url = (
f"{CHATWOOT_BASE}/api/v1/accounts/{CHATWOOT_ACCOUNT_ID}"
f"/conversations/{conversation_id}/messages"
)
headers = {"api_access_token": CHATWOOT_BOT_TOKEN}
payload = {
"content": content,
"message_type": OUTGOING,
"private": False,
}
async with httpx.AsyncClient(timeout=30.0) as client:
resp = await client.post(url, json=payload, headers=headers)
resp.raise_for_status()
async def handoff_to_human(conversation_id: int) -> None:
"""Toggle conversation status to open so human agents can take over."""
url = (
f"{CHATWOOT_BASE}/api/v1/accounts/{CHATWOOT_ACCOUNT_ID}"
f"/conversations/{conversation_id}/toggle_status"
)
headers = {"api_access_token": CHATWOOT_BOT_TOKEN}
payload = {"status": "open"}
async with httpx.AsyncClient(timeout=30.0) as client:
resp = await client.post(url, json=payload, headers=headers)
resp.raise_for_status()
@app.post("/chatwoot/webhook")
async def chatwoot_webhook(request: Request):
body = await request.body()
verify_chatwoot(request, body)
event = json.loads(body)
if event.get("event") != "message_created":
return {"status": "ignored"}
message = event.get("message", {})
conversation = event.get("conversation", {})
if conversation.get("status") != "pending":
return {"status": "ignored"}
if not is_incoming_message(message):
return {"status": "ignored"}
content = (message.get("content") or "").strip()
if not content:
return {"status": "ignored"}
conversation_id = conversation["id"]
conversation_key = f"chatwoot:wa:{conversation_id}"
reply = await ask_hermes(content, conversation_key=conversation_key)
if reply.upper().startswith("HANDOFF"):
await post_chatwoot_reply(
conversation_id,
"Connecting you with a human agent. Please hold.",
)
await handoff_to_human(conversation_id)
return {"status": "handoff"}
await post_chatwoot_reply(conversation_id, reply)
return {"status": "ok"}
Run with:
export CHATWOOT_BASE_URL=https://support.example.com
export CHATWOOT_ACCOUNT_ID=1
export CHATWOOT_BOT_TOKEN=<agent-bot-access-token>
export CHATWOOT_BOT_WEBHOOK_SECRET=<webhook-signing-secret-if-available>
export HERMES_API_KEY=<API_SERVER_KEY>
export HERMES_MODEL=hermes-agent
uvicorn bridge.main:app --host 0.0.0.0 --port 8080
Expose the bridge with HTTPS (reverse proxy or tunnel). Chatwoot must reach outgoing_url.
Session continuity
/v1/chat/completions is stateless — each request must carry the full messages[] history or the model forgets prior turns. The sample bridge uses POST /v1/responses with a stable conversation key (chatwoot:wa:{conversation_id}) so Hermes chains turns server-side.
| Header / parameter | Purpose |
|---|---|
conversation (Responses API body) | Multi-turn transcript continuity for this Chatwoot thread |
X-Hermes-Session-Key | Scopes long-term memory providers (e.g. Honcho) per channel — not transcript replay by itself |
X-Hermes-Session-Id | Transcript-scoped session ID when using /api/sessions/{id}/chat instead |
Alternatives if Responses API chaining is unavailable in your Hermes version:
POST /api/sessions/{id}/chat— create a Hermes session per Chatwoot conversation, persist the session ID in Chatwoot custom attributes- Replay Chatwoot message history into a stateless
/v1/chat/completionsmessages[]array
See Stateful vs Stateless Agents in Production for the general tradeoff framing.
Step 6: End-to-End Message Flow
1. Customer sends "Where is order 12345?" on WhatsApp
2. Meta POSTs webhook → Chatwoot /webhooks/whatsapp/<phone>
3. Chatwoot creates/updates conversation, fires Agent Bot webhook
4. Bridge receives message_created event
5. Bridge POSTs to Hermes `/v1/responses` with `conversation=chatwoot:wa:{id}` (OpenAI-backed)
6. Hermes may call tools (order lookup, knowledge search, etc.)
7. Bridge receives assistant text from the Responses `output` array
8. Bridge POSTs outgoing message to Chatwoot Messages API
9. Chatwoot sends via WhatsApp Cloud API → Meta → customer
Return HTTP 200 from the webhook handler as soon as you have validated the event. The sample bridge processes synchronously for clarity; if Hermes tool loops exceed Chatwoot’s webhook timeout, move ask_hermes to a background task and post the reply when the agent finishes.
Latency budget: Meta → Chatwoot → bridge → Hermes agent loop → Chatwoot → Meta. Tool-heavy Hermes turns can exceed WhatsApp UX expectations. For simple FAQ, restrict tools on the WhatsApp toolset; for complex lookups, send a quick “Looking that up…” ack first.
Human Handoff
Chatwoot’s value is the handoff path. Per the Agent Bot handoff docs, bot-owned conversations start in pending status. When the bot needs a human, call the Toggle Status API with {"status": "open"} using the agent bot access_token.
| Trigger | Action |
|---|---|
| User asks for human | Bridge posts handoff message; toggle_status → open |
| Hermes low confidence | Reply + offer human; optional private note for agents with trace summary |
| Agent takes over in Chatwoot UI | Conversation status becomes open; bridge ignores non-pending conversations |
| Escalation keywords | Bridge matches regex before calling Hermes |
After handoff, the bridge must stop auto-replying — check conversation.status == "pending" and message_type == 0 (incoming). Agents can return a conversation to the bot queue by toggling status back to pending.
WhatsApp Messaging Rules
These are Meta constraints, not Hermes or Chatwoot limits:
24-hour customer service window
Hermes’s WhatsApp Cloud docs note that replies more than 24 hours after the user’s last message require a pre-approved template. Chatwoot’s Messages API supports template_params for structured template messages.
Plan for:
- Free-form AI replies inside the 24h window
- Approved templates for re-engagement outside the window
Tokens and security
| Secret | Where | Purpose |
|---|---|---|
| Meta System User token | Chatwoot inbox config | Send/receive WhatsApp messages via Cloud API |
| Meta webhook verify token | Meta Developer Portal + Chatwoot inbox | GET webhook verification handshake |
Meta App Secret (WHATSAPP_APP_SECRET) | Hermes direct path only (hermes whatsapp-cloud) | Validates Meta webhook signatures when Hermes receives webhooks directly |
Chatwoot agent bot access_token | Bridge service | Post replies and toggle conversation status |
| Chatwoot bot webhook signing secret | Bridge service (optional) | Verify X-Chatwoot-Signature on inbound Agent Bot events |
Hermes API_SERVER_KEY | Bridge service + Hermes ~/.hermes/.env | Authenticate calls to /v1/chat/completions |
| OpenAI API key | Hermes ~/.hermes/.env only | LLM provider for the agent loop |
Never put OpenAI or Hermes keys in Chatwoot’s frontend or the bridge’s client-side code.
Alternative: Hermes Direct (No Chatwoot)
If you do not need a team inbox, skip Chatwoot entirely:
hermes whatsapp-cloud # guided wizard
hermes gateway # starts Cloud API webhook on port 8090
Use Cloudflare Tunnel to expose https://<tunnel>/whatsapp/webhook to Meta. Configure OPENAI_API_KEY, set WHATSAPP_CLOUD_ALLOWED_USERS, and you have a direct Hermes ↔ WhatsApp ↔ OpenAI loop.
Trade-off: no agent dashboard, no human handoff UI, no conversation analytics in Chatwoot. Faster to stand up for personal bots.
Deployment Topology
┌─────────────────────────────────────────────────────────┐
│ VPS / container host │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ Hermes │ │ Bridge │ │ (optional) │ │
│ │ gateway + │◄──│ FastAPI │ │ nginx/Caddy │ │
│ │ API :8642 │ │ :8080 │ │ TLS proxy │ │
│ └──────────────┘ └──────┬───────┘ └─────────────┘ │
│ │ │
└─────────────────────────────┼────────────────────────────┘
│ HTTPS
v
┌──────────────────┐
│ Chatwoot (Cloud │
│ or self-hosted) │
└────────┬─────────┘
│
v
┌──────────────────┐
│ Meta WhatsApp │
│ Cloud API │
└──────────────────┘
Minimum viable deployment:
- Chatwoot on a managed host or self-hosted with TLS
- Hermes + bridge on a host with a public HTTPS ingress (VM, container, or tunnel)
- System User token (not 24h temp token)
- Process supervisor for
hermes gatewayanduvicorn
Operational Checklist
[ ] Meta app subscribed to WABA (subscribed_apps curl)
[ ] Webhook messages field enabled in Meta dashboard
[ ] Chatwoot inbox receives inbound test message
[ ] Agent Bot webhook fires on message_created
[ ] Bridge verifies webhook signatures when a signing secret is configured
[ ] Hermes API health check passes with API_SERVER_KEY
[ ] OpenAI billing and model access confirmed
[ ] End-to-end reply delivered to WhatsApp
[ ] Handoff path tested (human agent can take conversation)
[ ] 24h window + template message path documented for your team
[ ] Secrets rotated; no tokens in git
Who owns what (ops)
- Meta token rotation — platform/DevOps; System User token in Chatwoot inbox config
- WhatsApp template approval — support ops + marketing; templates for re-engagement outside the 24h window
- Escalation SLA — support lead; define when the bridge must
toggle_status→open - Conversation audit — Chatwoot for customer-visible threads; Hermes session DB for agent reasoning traces
Common Failure Modes
| Symptom | Likely cause | Fix |
|---|---|---|
| Webhook verifies but no messages in Chatwoot | App not subscribed to WABA | subscribed_apps POST |
Outbound fails #131030 | Dev-mode number format mismatch | Align allowed-list and stored contact format |
| Bridge 401 from Hermes | Wrong API_SERVER_KEY | Match bridge HERMES_API_KEY to Hermes env |
| Double replies | Bridge handling outgoing messages or open conversations | Filter message_type == 0 and status == "pending" |
| Empty Hermes response | Model/provider misconfig | hermes model + hermes chat -q "test" locally |
| Chatwoot “reauthorization required” | Token expired or app mode | Refresh System User token; check Meta app live mode |
| Slow replies | Hermes tool loop on WhatsApp | Narrow to hermes-whatsapp toolset; ack first on slow lookups |
| Bot forgets prior turns | Used stateless /v1/chat/completions without history | Switch to /v1/responses conversation key or /api/sessions/{id}/chat |
Limitations of This Minimal Bridge
Before treating the sample as production-ready, know what it omits:
- No idempotency — Chatwoot webhook retries can produce duplicate replies unless you dedupe on
message.idorX-Chatwoot-Delivery. - Synchronous Hermes call — tool-heavy turns may exceed webhook timeouts; process async in production.
- No rate limiting — bridge accepts every
message_createdevent; add per-conversation throttling. - Signature verification is release-dependent — confirm Agent Bot signing on your Chatwoot version (#13809).
- No PII redaction in logs — scrub customer message content before logging bridge payloads.
What to Add Next
This article covers the wiring. Support bots usually add:
- Chatwoot → Hermes session mapping in Postgres/Redis
- Proper handoff via Chatwoot conversation assignment APIs
- Eval traces (did the bot answer from the right source?)
- Rate limits on the bridge
- Hermes skills for your support macros and refund policy
- Template message library for outbound campaigns outside the 24h window
Bottom Line
Building a WhatsApp agent with Cloud API, Chatwoot, Hermes, and OpenAI is a composition problem, not a single-product install:
- Chatwoot owns the WhatsApp webhook and team inbox
- Hermes + OpenAI own the agent brain, tools, and memory
- A thin bridge connects Chatwoot’s Agent Bot protocol to Hermes’s OpenAI-compatible API
That split gives you official WhatsApp Cloud API messaging, human handoff, and a real agent harness — without forcing one tool to be all three.
Sources
Primary sources consulted on July 3, 2026:
- Hermes Agent — WhatsApp Business (Cloud API) — direct webhook path, env vars (
WHATSAPP_APP_SECRET, etc.), 24h window. hermes-agent.nousresearch.com/docs/user-guide/messaging/whatsapp-cloud - Hermes Agent — Environment Variables Reference —
API_SERVER_KEY, gateway, and provider env vars. hermes-agent.nousresearch.com/docs/reference/environment-variables - Hermes Agent — Messaging Gateway — platform capabilities,
hermes-whatsapptoolset. hermes-agent.nousresearch.com/docs/user-guide/messaging/ - Hermes Agent — API Server — OpenAI-compatible endpoint, Responses API
conversationchaining,X-Hermes-Session-Key, Sessions API. hermes-agent.nousresearch.com/docs/user-guide/features/api-server - Hermes Agent — AI Providers —
OPENAI_API_KEY,openai-apiprovider. hermes-agent.nousresearch.com/docs/integrations/providers - Hermes Agent — Configuration —
config.yaml, model provider setup. hermes-agent.nousresearch.com/docs/user-guide/configuration - Chatwoot — How to set up a WhatsApp channel (manual flow) — Cloud API inbox, webhook URL format, verify token. chatwoot.com/hc/user-guide/articles/1756799850-how-to-setup-a-whats_app-channel-manual-flow
- Chatwoot — How to use Agent bots — webhook events, handoff via
toggle_status, inbox connection. chatwoot.com/hc/user-guide/articles/1677497472-how-to-use-agent-bots - Chatwoot — How to use webhooks —
X-Chatwoot-Signature/X-Chatwoot-TimestampHMAC verification. chatwoot.com/hc/user-guide/articles/1677693021-how-to-use-webhooks - Chatwoot Developer Docs — Create Agent Bot — API payload and
access_token. developers.chatwoot.com/api-reference/account-agentbots/create-an-agent-bot - Chatwoot Developer Docs — Toggle Status —
POST .../toggle_statuswith{"status": "open"}. developers.chatwoot.com/api-reference/conversations/toggle-status - Chatwoot Developer Docs — Create New Message — outgoing replies, WhatsApp
template_params. developers.chatwoot.com/api-reference/messages/create-new-message - Meta — WhatsApp embedded signup webhooks — WABA subscription context. developers.facebook.com/docs/whatsapp/embedded-signup/webhooks
Issue-tracker evidence (secondary, for operational pitfalls):
- Chatwoot #11725 —
subscribed_appsand inbound message delivery. github.com/chatwoot/chatwoot/issues/11725 - Chatwoot #13097 —
subscribed_appsfix for missing inbound messages. github.com/chatwoot/chatwoot/issues/13097 - Chatwoot #13932 — dev-mode phone number format
#131030. github.com/chatwoot/chatwoot/issues/13932 - Chatwoot #13809 — Agent Bot webhook signing secret vs
access_tokendistinction. github.com/chatwoot/chatwoot/issues/13809
Series:
- Part 1: Hermes Agent architecture and RAG lessons
- RAG Apps in Practice — if support bots need document grounding with eval gates
- Stateful vs Stateless Agents in Production — session continuity tradeoffs