WhatsApp (Cloud API & Web)
WhatsApp Cloud API webhook mode and native WhatsApp Web mode, and how Revka picks between them.
Revka talks to WhatsApp in two completely different ways, and both live under the same [channels_config.whatsapp] section in ~/.revka/config.toml. Cloud API mode is Meta’s official WhatsApp Business Cloud API: messages arrive over a public HTTPS webhook and replies go out through Meta’s Graph API. Web mode is a native WhatsApp Web client (the wa-rs library) that links to a phone number the way the WhatsApp Web app does — no Meta Business account, no public port, end-to-end encrypted over the Signal Protocol. This page covers both, the WATI-hosted alternative, and the rule Revka uses to decide which backend to run.
The selector is the config: set phone_number_id for Cloud API, or session_path for Web. You do not pick a mode with a flag — Revka negotiates it at startup from which key you set. If you have not connected a channel before, read Connect a messaging channel for the allowlist model, and the Channels overview for the shared trait model.
How Revka picks a mode
Section titled “How Revka picks a mode”At daemon startup the WhatsApp config is inspected and a backend is chosen:
| Condition | Backend selected |
|---|---|
phone_number_id is set | Cloud API mode (WhatsAppChannel) |
session_path is set (and no phone_number_id) | Web mode (WhatsAppWebChannel) |
| Both are set | Cloud API mode is preferred, with a startup warning to remove one selector |
| Neither is set | The channel is skipped, with a config-invalid warning |
# Cloud API mode — phone_number_id is the selector[channels_config.whatsapp]phone_number_id = "123456789012345"
# Web mode — session_path is the selector[channels_config.whatsapp]session_path = "~/.revka/whatsapp-session.db"WhatsApp Cloud API (webhook mode)
Section titled “WhatsApp Cloud API (webhook mode)”Cloud API mode is push-based: it requires a public HTTPS callback URL because Meta delivers inbound messages to your gateway. Internally, the channel’s listen() method is a no-op placeholder — the real ingress is the gateway’s /whatsapp webhook. Outbound replies are sent to https://graph.facebook.com/v18.0/{phone_number_id}/messages.
Use this mode when you have a Meta Business account and a verified WhatsApp Business phone number, and when you can expose the gateway publicly (directly or through a tunnel).
[channels_config.whatsapp]access_token = "EAAB..." # required, from Meta Business Suitephone_number_id = "123456789012345" # required — selects Cloud API modeverify_token = "your-verify-token" # required — you define it; Meta echoes it backapp_secret = "your-app-secret" # recommended — enables HMAC payload verificationallowed_numbers = ["*"] # tighten after testing# dm_mention_patterns = [] # optional regex gating for DMs# group_mention_patterns = [] # optional regex gating for group chats# proxy_url = "socks5://127.0.0.1:9050"| Field | Type / values | Default | Meaning |
|---|---|---|---|
access_token | string (required) | — | Cloud API access token from Meta Business Suite. Transmitted only over HTTPS (enforced at startup). |
phone_number_id | string (required) | — | Phone number ID from the Meta Business API. Its presence selects Cloud API mode. |
verify_token | string (required) | — | A token you choose. Meta sends it back during the webhook verification handshake; Revka compares it in constant time. |
app_secret | string | — | Meta app secret used to verify the X-Hub-Signature-256 HMAC on inbound payloads. Strongly recommended in production. |
allowed_numbers | list | [] (deny all) | E.164 phone numbers (+15551234567) or "*". |
dm_mention_patterns | list of regex | [] | Case-insensitive patterns; when non-empty, only matching DMs are processed and matched fragments are stripped. ReDoS-guarded (64 KiB compiled size limit). |
group_mention_patterns | list of regex | [] | Same as above, for group chats. |
proxy_url | string | — | Per-channel HTTP/HTTPS/SOCKS5 proxy, overriding the global [proxy]. |
Webhook endpoints
Section titled “Webhook endpoints”The gateway exposes the Meta webhook as two routes. Point your Meta app’s callback URL at https://<your-gateway>/whatsapp.
GET /whatsapp # Meta hub verification handshakePOST /whatsapp # inbound message deliveryVerification (GET). Meta calls GET /whatsapp with hub.mode, hub.verify_token, and hub.challenge query parameters. When hub.mode=subscribe and the token matches your configured verify_token (constant-time compare), the gateway echoes back the hub.challenge value to complete subscription.
GET /whatsapp?hub.mode=subscribe&hub.verify_token=your-verify-token&hub.challenge=1234567890→ 200 OK1234567890Message delivery (POST). Meta posts the standard WhatsApp Cloud API payload. The gateway validates the signature (see below), filters the sender against allowed_numbers, and routes the message to the agent.
App-secret HMAC verification
Section titled “App-secret HMAC verification”When an app secret is configured, the gateway verifies the X-Hub-Signature-256 header on every inbound POST before processing it:
- The header value has the form
sha256=<hex>. - Revka computes
HMAC-SHA256(app_secret, raw_request_body)and compares it to the decoded hex using a constant-time check. - A missing, malformed, or mismatched signature causes the request to be rejected.
The app secret is resolved with environment variable taking priority over config:
# Preferred: keep the secret out of config.tomlexport REVKA_WHATSAPP_APP_SECRET="your-app-secret"If neither REVKA_WHATSAPP_APP_SECRET nor the app_secret config key is set, signature verification is skipped — acceptable for local testing, but you should configure the app secret for any internet-facing deployment.
WhatsApp Web (wa-rs) feature: whatsapp-web
Section titled “WhatsApp Web (wa-rs) ”Web mode embeds a native Rust WhatsApp Web client (wa-rs) with full Baileys parity: QR-code or pair-code linking, end-to-end encryption via the Signal Protocol, groups, media, presence indicators, reactions, and message editing/deletion. It links to a phone number the same way the WhatsApp Web browser app does — no Meta Business account and no public inbound port. Voice transcription and TTS replies are supported via the shared [transcription] and [tts] sections.
Web mode is gated behind a Cargo feature and is not in the default build:
cargo build --release --features whatsapp-webIf session_path is configured but the binary was built without the feature, the daemon logs a warning and prints a rebuild hint instead of starting the channel.
[channels_config.whatsapp]session_path = "~/.revka/whatsapp-session.db" # required — selects Web modepair_phone = "15551234567" # optional — enables pair-code linkingpair_code = "" # optional — custom code; blank = auto-generatedallowed_numbers = ["*"] # tighten after testingmode = "business" # business | personal# dm_policy = "allowlist" # personal mode only# group_policy = "allowlist" # personal mode only# self_chat_mode = false # personal mode only# dm_mention_patterns = []# group_mention_patterns = []# proxy_url = "socks5://127.0.0.1:9050"| Field | Type / values | Default | Meaning |
|---|---|---|---|
session_path | string (required) | — | SQLite session store path. Its presence selects Web mode. Must be on persistent storage — losing it forces a relink. |
pair_phone | string | — | Phone number (country code + number, e.g. 15551234567) for pair-code linking. Omit to use the QR-code flow instead. |
pair_code | string | — | Custom pair code. Leave blank to let WhatsApp generate one. |
allowed_numbers | list | [] (deny all) | E.164 phone numbers or "*". |
mode | "business" | "personal" | "business" | business responds to everything passing the allowlist. personal applies the per-chat-type policies below. |
dm_policy | "allowlist" | "ignore" | "all" | "allowlist" | DM handling when mode = "personal". |
group_policy | "allowlist" | "ignore" | "all" | "allowlist" | Group-chat handling when mode = "personal". |
self_chat_mode | bool | false | When mode = "personal", always respond in your own self-chat (Notes to Self). |
dm_mention_patterns | list of regex | [] | Case-insensitive DM mention gating; matched fragments stripped from forwarded content. |
group_mention_patterns | list of regex | [] | Case-insensitive group mention gating. |
proxy_url | string | — | Per-channel proxy override. |
Pair-code linking
Section titled “Pair-code linking”Pair-code linking lets you link the device by entering a short code in the WhatsApp app instead of scanning a QR image — useful on headless servers.
- Set
session_pathandpair_phone(your number in country-code-plus-number form, no+). Optionally set a custompair_code. - Build with the feature and start the daemon:
cargo build --release --features whatsapp-webthenrevka daemon. - On startup the client requests a pairing code for
pair_phone. In WhatsApp on your phone, open Settings → Linked Devices → Link a Device → Link with phone number instead, and enter the code. - Once linked, the encrypted session is written to
session_path. Subsequent restarts reuse it — no relinking needed unless the file is lost.
If you omit pair_phone, the client falls back to the QR-code flow: scan the code shown at startup from Linked Devices in the WhatsApp app.
WATI (hosted WhatsApp Business API)
Section titled “WATI (hosted WhatsApp Business API)”WATI is a separate, hosted WhatsApp Business API provider with its own config section, [channels_config.wati] — not part of the whatsapp selector logic. Like Cloud API mode it is webhook-based and needs a public HTTPS endpoint, but it uses WATI’s API format. Inbound arrives at the gateway /wati endpoint; outbound text is sent to WATI’s conversations endpoint. Audio transcription (up to 25 MB) is supported, and media downloads are SSRF-protected (the media host must match the configured api_url host).
[channels_config.wati]api_token = "wati-bearer-token" # requiredapi_url = "https://live-mt-server.wati.io" # requiredtenant_id = "my-tenant" # optional — prefix for recipient routingallowed_numbers = ["*"] # tighten after testing# proxy_url = "socks5://127.0.0.1:9050"| Field | Type / values | Default | Meaning |
|---|---|---|---|
api_token | string (required) | — | WATI bearer token. |
api_url | string (required) | — | WATI server base URL (e.g. https://live-mt-server.wati.io). |
tenant_id | string | — | Optional tenant prefix for recipient targeting. |
allowed_numbers | list | [] (deny all) | E.164 phone numbers or "*". |
proxy_url | string | — | Per-channel proxy override. |
GET /wati # WATI endpoint verification (echoes hub.challenge)POST /wati # inbound message delivery (allowed-numbers filtered)Choose WATI when you already run WhatsApp through WATI’s platform and want Revka to ride on top of it rather than connecting to Meta’s Cloud API directly.
Choosing a mode
Section titled “Choosing a mode”| Cloud API | WhatsApp Web (wa-rs) | WATI | |
|---|---|---|---|
| Config selector | phone_number_id | session_path | [channels_config.wati] |
| Receive mode | webhook (push) | WebSocket (native client) | webhook (push) |
| Public inbound port | Required | Not required | Required |
| Meta Business account | Required | Not required | Via WATI |
| Linking | n/a | QR or pair code | n/a |
| Encryption | TLS in transit | E2EE (Signal Protocol) | TLS in transit |
| Feature flag | none (default build) | whatsapp-web | none (default build) |
| Payload auth | app_secret HMAC | session keys | WATI token |
- Pick Cloud API for an official, supported integration with a Business account when you can expose an HTTPS webhook.
- Pick Web mode to link an existing personal or business number with no Meta account and no public port — at the cost of building the
whatsapp-webfeature and safeguarding the session database. - Pick WATI if your WhatsApp number is already managed through WATI.
Verify and troubleshoot
Section titled “Verify and troubleshoot”revka doctor # checks channel config and connectivityrevka status # shows configured channels and health- Cloud API webhook fails verification — confirm the callback URL is
https://<gateway>/whatsapp, thatverify_tokenmatches exactly what you entered in the Meta app, and that the gateway is reachable over HTTPS. - Inbound POSTs are rejected — if you set an app secret, ensure Meta’s
X-Hub-Signature-256is being passed through unmodified by any proxy; HMAC is computed over the raw body. - Web mode does not start — the binary must be built with
--features whatsapp-web; check the daemon log for the rebuild hint. - Senders are ignored —
allowed_numbersis empty (deny all) by default; add the E.164 number or"*".