Kapso WhatsApp platform adapter for Hermes Agent. It receives Kapso platform
webhooks, turns inbound WhatsApp messages into Hermes MessageEvents, and sends
Hermes replies through Kapso's WhatsApp Cloud API proxy.
- A Hermes Agent install with the gateway enabled.
- A Kapso account. Sign up at kapso.ai, create or open a project, and create a project API key in the Kapso dashboard.
- A connected Kapso WhatsApp number. If you do not have one yet, run
kapso setupafter installing the Kapso CLI, or complete WhatsApp onboarding in the Kapso dashboard. - A public HTTPS URL that can reach the Hermes Kapso adapter. Tailscale Funnel
works well and should proxy to
http://127.0.0.1:8648. - Node.js/npm if you want the setup command to install and use the Kapso CLI.
Install and enable the plugin from GitHub:
hermes plugins install gokapso/hermes-agent-plugin --enableThe installer prompts only for KAPSO_API_KEY. Webhook setup happens in the
next step.
Hermes clones Git plugins into ~/.hermes/plugins/kapso and loads them on the
next session or gateway restart.
Recommended setup:
hermes kapso setup --install-cli --funnel-url https://<your-funnel-host>That command can:
- install the Kapso CLI with
npm install -g @kapso/cli - list connected WhatsApp numbers from Kapso and ask which one Hermes should use
- ask for your WhatsApp user number/wa_id for
KAPSO_HOME_CHANNELandKAPSO_ALLOWED_USERS - generate and save
KAPSO_WEBHOOK_SECRET - create the Kapso phone-number webhook with payload version
v2
The webhook created by the helper points at:
https://<your-funnel-host>/kapso/webhook
For a non-interactive setup:
hermes kapso setup \
--api-key "$KAPSO_API_KEY" \
--phone-number-id "1041695002363992" \
--home-channel "15551234567" \
--allowed-users "15551234567" \
--funnel-url "https://<your-funnel-host>" \
--configure-webhook \
--install-cli \
--no-promptRestart the gateway after setup:
hermes gateway restart
hermes gateway statusExpose the adapter to Kapso. The adapter listens on 0.0.0.0:8648 and accepts
POST /kapso/webhook by default. With Tailscale Funnel, point the public URL at
that local port:
tailscale funnel reset
tailscale funnel --bg http://127.0.0.1:8648
tailscale funnel statusSet the Kapso webhook URL to:
https://<your-funnel-host>/kapso/webhook
Recommended Kapso webhook settings:
| Setting | Value |
|---|---|
| Events | whatsapp.message.received |
| Payload version | v2 |
| Secret | Same value as KAPSO_WEBHOOK_SECRET |
Verify the setup:
hermes kapso status
curl http://127.0.0.1:8648/health
curl https://<your-funnel-host>/healthIf CLI webhook creation fails, create the webhook manually in Kapso with these settings:
| Setting | Value |
|---|---|
| Scope | WhatsApp phone number |
| Endpoint URL | https://<your-funnel-host>/kapso/webhook |
| Events | whatsapp.message.received |
| Kind | kapso |
| Payload version | v2 |
| Secret | Same value as KAPSO_WEBHOOK_SECRET |
The setup command writes these values to ~/.hermes/.env:
KAPSO_API_KEY=...
KAPSO_WEBHOOK_SECRET=...
KAPSO_PHONE_NUMBER_ID=... # recommended for outbound and cron delivery
KAPSO_HOME_CHANNEL=15551234567 # optional default recipient for deliver=kapso
KAPSO_ALLOWED_USERS=15551234567 # recommended for production allowlisting
KAPSO_WEBHOOK_URL=https://... # status/debug onlyTo allow a specific WhatsApp user later:
hermes kapso setup --allowed-users 15551234567 --no-prompt
hermes gateway restartFor development-only open access:
hermes kapso setup --allow-all-users --no-prompt
hermes gateway restartUseful Kapso CLI checks after --install-cli:
kapso status
kapso whatsapp numbers list --output jsonSignature verification is on by default. For unsigned local fixtures only:
KAPSO_VERIFY_WEBHOOK_SIGNATURES=falseInbound WhatsApp voice notes use Kapso's transcript when the webhook payload
includes one. In that path, the adapter sends the transcript text straight to
Hermes and skips audio attachment, so users do not need faster-whisper or an
OpenAI transcription key for those messages.
If Kapso has not provided a transcript in the webhook payload, the adapter falls
back to downloading the voice note through Kapso, caching it as .ogg, and
passing it to Hermes as audio media. Hermes then transcribes it with its
configured STT provider before the agent turn.
For local, no-key transcription with faster-whisper:
sudo apt update
sudo apt install -y ffmpeg python3-venv python3-pip
~/.hermes/hermes-agent/venv/bin/python -m ensurepip --upgrade || true
~/.hermes/hermes-agent/venv/bin/python -m pip install -U pip setuptools wheel
~/.hermes/hermes-agent/venv/bin/python -m pip install -U faster-whisper
hermes config set stt.provider local
hermes config set stt.local.model base
hermes gateway restartIf the Hermes venv has no pip and ensurepip is unavailable:
curl -sS https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py
~/.hermes/hermes-agent/venv/bin/python /tmp/get-pip.py
~/.hermes/hermes-agent/venv/bin/python -m pip install -U faster-whisperFor OpenAI transcription:
printf '\nVOICE_TOOLS_OPENAI_KEY=sk-...\n' >> ~/.hermes/.env
hermes config set stt.provider openai
hermes config set stt.openai.model gpt-4o-mini-transcribe
hermes gateway restartOther OpenAI model options include whisper-1 and gpt-4o-transcribe.
Verify voice-note processing:
tail -f ~/.hermes/logs/gateway.log ~/.hermes/logs/agent.log \
| grep --line-buffered -iE 'kapso|audio|voice|stt|transcrib|transcript|whisper|openai'Healthy Kapso-transcript logs look like:
[kapso] using Kapso transcript for inbound audio ...
Healthy STT fallback logs look like:
[kapso] cached inbound audio ... at ~/.hermes/audio_cache/audio_....ogg
Transcribed audio_....ogg via local whisper (base, lang=en, ...)
Inbound WhatsApp documents are downloaded through Kapso, cached locally, and attached to Hermes as document media. Hermes receives the local file path so it can inspect PDFs, DOCX/XLSX/PPTX files, archives, and other supported document types with its normal document/tooling flow.
Small text-like documents (.txt, .md, .csv, .json, .yaml, and similar)
are also injected into the message text when they are under 100 KB, so the agent
can answer simple questions without opening an extra file tool.
For PDFs and richer document formats, Hermes currently receives the cached file path and may ask for tool approval before extracting or inspecting the file. That approval prompt means the document reached Hermes; it is Hermes' document tooling path, not a Kapso webhook failure. Native "send this PDF directly to the model" behavior depends on Hermes core/provider document routing and is not enabled by this adapter alone.
Inbound sessions use encoded IDs:
kapso:<base64url(phone_number_id)>:<base64url(wa_id)>[:<base64url(conversation_id)>]
For manual sends or cron delivery, you can use either a plain WhatsApp recipient
when KAPSO_PHONE_NUMBER_ID is configured:
15551234567
or an explicit phone number ID and recipient:
<phone_number_id>:15551234567
Hermes outbound MEDIA:/absolute/path attachments are sent through Kapso's Meta
proxy, then delivered with the returned WhatsApp media ID.
Images, audio, video, and documents use the matching WhatsApp media message type.
Other local files are sent as WhatsApp document messages. WhatsApp stickers are
supported only when explicitly sent as sticker media; Hermes MEDIA:/path.webp
attachments are treated as images by Hermes.
Environment variables are the easiest path, but this also works:
gateway:
platforms:
kapso:
enabled: true
extra:
api_key: "..."
webhook_secret: "..."
phone_number_id: "..."If Hermes is not receiving messages:
hermes gateway status
hermes kapso status
curl http://127.0.0.1:8648/health
tailscale funnel status
journalctl --user -u hermes-gateway.service -fCheck that Tailscale Funnel proxies to http://127.0.0.1:8648, not an older
bridge process. The public webhook should end in /kapso/webhook.
Check that Kapso is sending whatsapp.message.received events with payload
version v2, and that the webhook secret in Kapso matches
KAPSO_WEBHOOK_SECRET.
If the gateway log says no user allowlist is configured, add your WhatsApp ID:
hermes kapso setup --allowed-users 15551234567 --no-prompt
hermes gateway restartIf outbound replies fail, confirm KAPSO_API_KEY and KAPSO_PHONE_NUMBER_ID
are set:
hermes kapso status
kapso whatsapp numbers list --output jsonIf images do not reach the agent, tail the gateway logs while sending a photo:
journalctl --user -u hermes-gateway.service -f | grep -i kapsoSuccessful image ingestion logs cached inbound image ... and writes the file
under ~/.hermes/image_cache or ~/.hermes/cache/images, depending on the
Hermes runtime version. If you only see image message ... has no downloadable media URL yet, confirm the webhook payload includes either
kapso.mediaUrl/kapso.media_url or an image media id.
If voice notes do not transcribe, first confirm whether Kapso supplied a transcript or the plugin cached the audio fallback:
grep -R "using Kapso transcript\|cached inbound audio\|User sent audio\|STT" ~/.hermes/logs/*.log
find ~/.hermes/cache/audio ~/.hermes/audio_cache -type f -mmin -10 -ls 2>/dev/nullSuccessful Kapso transcript ingestion logs using Kapso transcript ... and does
not require a local or OpenAI STT provider. If no transcript is present,
successful fallback ingestion logs cached inbound audio .... The cached file
is usually under ~/.hermes/audio_cache or ~/.hermes/cache/audio. Hermes then
uses its configured STT provider to transcribe the cached file. For a no-key
local STT provider:
~/.hermes/hermes-agent/venv/bin/python -m pip install -U faster-whisper
hermes gateway restartFor OpenAI Whisper/transcribe instead, add VOICE_TOOLS_OPENAI_KEY to
~/.hermes/.env and restart the gateway. If old logs show voice notes cached
as .opus, update the plugin; Kapso voice notes are cached as .ogg for
OpenAI STT compatibility.
If documents do not reach the agent, confirm they were cached:
grep -R "cached inbound document" ~/.hermes/logs/*.log
find ~/.hermes/cache/documents ~/.hermes/document_cache -type f -mmin -10 -ls 2>/dev/nullSuccessful document ingestion logs cached inbound document .... The cached
file is usually under ~/.hermes/document_cache or
~/.hermes/cache/documents, depending on the Hermes runtime version.
If the agent responds with a dangerous-command approval prompt while reading a PDF, the document was delivered correctly and Hermes is trying to run its local extraction/inspection tool. Approving that command allows Hermes to read the cached file. Denying it leaves the agent with only the filename and caption.
hermes plugins install ... --enableprompts only forKAPSO_API_KEY.hermes kapso setuphandles the webhook secret and phone number setup.hermes kapso setup --install-cliis safe to rerun when you need to rotate keys or addKAPSO_PHONE_NUMBER_ID.- Hermes can run the Kapso CLI after installation, so you can ask the agent to inspect numbers or webhook state. Enter secrets through the installer/setup prompts rather than pasting them into chat.
- Text messages are split at WhatsApp's 4096-character limit.
- Outbound Markdown links are converted to
label (url), and**bold**is converted to WhatsApp's*bold*style. - Inbound images are downloaded through Kapso, cached locally, and attached to
Hermes
MessageEvent.media_urlsfor native vision processing. - Inbound audio and voice notes are downloaded through Kapso, cached locally,
and attached to Hermes
MessageEvent.media_urlsfor native STT processing. - Inbound documents are downloaded through Kapso, cached locally, and attached
to Hermes
MessageEvent.media_urlsfor native document processing. - Other unsupported media currently lands as captions or descriptive placeholders.