Skip to content

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.

At daemon startup the WhatsApp config is inspected and a backend is chosen:

ConditionBackend selected
phone_number_id is setCloud API mode (WhatsAppChannel)
session_path is set (and no phone_number_id)Web mode (WhatsAppWebChannel)
Both are setCloud API mode is preferred, with a startup warning to remove one selector
Neither is setThe 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"

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 Suite
phone_number_id = "123456789012345" # required — selects Cloud API mode
verify_token = "your-verify-token" # required — you define it; Meta echoes it back
app_secret = "your-app-secret" # recommended — enables HMAC payload verification
allowed_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"
FieldType / valuesDefaultMeaning
access_tokenstring (required)Cloud API access token from Meta Business Suite. Transmitted only over HTTPS (enforced at startup).
phone_number_idstring (required)Phone number ID from the Meta Business API. Its presence selects Cloud API mode.
verify_tokenstring (required)A token you choose. Meta sends it back during the webhook verification handshake; Revka compares it in constant time.
app_secretstringMeta app secret used to verify the X-Hub-Signature-256 HMAC on inbound payloads. Strongly recommended in production.
allowed_numberslist[] (deny all)E.164 phone numbers (+15551234567) or "*".
dm_mention_patternslist 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_patternslist of regex[]Same as above, for group chats.
proxy_urlstringPer-channel HTTP/HTTPS/SOCKS5 proxy, overriding the global [proxy].

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 handshake
POST /whatsapp # inbound message delivery

Verification (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 OK
1234567890

Message 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.

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:

Terminal window
# Preferred: keep the secret out of config.toml
export 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:

Terminal window
cargo build --release --features whatsapp-web

If 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 mode
pair_phone = "15551234567" # optional — enables pair-code linking
pair_code = "" # optional — custom code; blank = auto-generated
allowed_numbers = ["*"] # tighten after testing
mode = "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"
FieldType / valuesDefaultMeaning
session_pathstring (required)SQLite session store path. Its presence selects Web mode. Must be on persistent storage — losing it forces a relink.
pair_phonestringPhone number (country code + number, e.g. 15551234567) for pair-code linking. Omit to use the QR-code flow instead.
pair_codestringCustom pair code. Leave blank to let WhatsApp generate one.
allowed_numberslist[] (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_modeboolfalseWhen mode = "personal", always respond in your own self-chat (Notes to Self).
dm_mention_patternslist of regex[]Case-insensitive DM mention gating; matched fragments stripped from forwarded content.
group_mention_patternslist of regex[]Case-insensitive group mention gating.
proxy_urlstringPer-channel proxy override.

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.

  1. Set session_path and pair_phone (your number in country-code-plus-number form, no +). Optionally set a custom pair_code.
  2. Build with the feature and start the daemon: cargo build --release --features whatsapp-web then revka daemon.
  3. 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.
  4. 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 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" # required
api_url = "https://live-mt-server.wati.io" # required
tenant_id = "my-tenant" # optional — prefix for recipient routing
allowed_numbers = ["*"] # tighten after testing
# proxy_url = "socks5://127.0.0.1:9050"
FieldType / valuesDefaultMeaning
api_tokenstring (required)WATI bearer token.
api_urlstring (required)WATI server base URL (e.g. https://live-mt-server.wati.io).
tenant_idstringOptional tenant prefix for recipient targeting.
allowed_numberslist[] (deny all)E.164 phone numbers or "*".
proxy_urlstringPer-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.

Cloud APIWhatsApp Web (wa-rs)WATI
Config selectorphone_number_idsession_path[channels_config.wati]
Receive modewebhook (push)WebSocket (native client)webhook (push)
Public inbound portRequiredNot requiredRequired
Meta Business accountRequiredNot requiredVia WATI
Linkingn/aQR or pair coden/a
EncryptionTLS in transitE2EE (Signal Protocol)TLS in transit
Feature flagnone (default build)whatsapp-webnone (default build)
Payload authapp_secret HMACsession keysWATI 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-web feature and safeguarding the session database.
  • Pick WATI if your WhatsApp number is already managed through WATI.
Terminal window
revka doctor # checks channel config and connectivity
revka status # shows configured channels and health
  • Cloud API webhook fails verification — confirm the callback URL is https://<gateway>/whatsapp, that verify_token matches 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-256 is 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 ignoredallowed_numbers is empty (deny all) by default; add the E.164 number or "*".