Webhook ingress
Generic webhook, WhatsApp, Linq, WATI, Nextcloud Talk, Gmail push, and SOP webhooks with signing and idempotency.
The gateway exposes a set of public HTTP ingress paths that let external systems push messages into your agent. These are distinct from the bearer-authenticated /api/* surface: each webhook has its own authentication scheme — a shared secret, an HMAC signature, or a provider hub handshake — tuned to the platform that calls it. Use these endpoints to wire a messaging provider, an email push subscription, or any third-party system into Revka over plain HTTP POST.
Reach for this page when you are configuring a channel that delivers messages by webhook (WhatsApp Cloud API, WATI, Linq, Nextcloud Talk, Gmail push), exposing a generic integration endpoint, or routing a Standard Operating Procedure off an inbound HTTP event. Every webhook channel needs a publicly reachable HTTPS URL — see Expose your gateway with a tunnel before you start. For the channel side of each integration, see Channels overview; for the bearer-authenticated REST surface, see the Gateway API overview.
Generic webhook
Section titled “Generic webhook”The simplest integration path. POST a JSON message and the gateway runs it as an agent turn.
POST /webhookContent-Type: application/json{ "message": "Hello agent" }The message field is required; any other JSON shape returns 400. The response carries the agent’s reply.
Authentication
Section titled “Authentication”The generic webhook is gated by two independent, stackable layers:
- Pairing bearer token. When
[gateway].require_pairing = true(the default),/webhookrequires the sameAuthorization: Bearer rk_<token>as the rest of the API. An unpaired or invalid token returns401. Setrequire_pairing = falseonly for a gateway bound to localhost on a trusted machine. - Webhook secret (optional). If a webhook secret is configured, the caller must also send it in the
X-Webhook-Secretheader. The configured secret is SHA-256 hashed at startup and never stored in plaintext; the incoming header is hashed and compared in constant time. A missing or wrong value returns401.
POST /webhookAuthorization: Bearer rk_<token>X-Webhook-Secret: <shared-secret>X-Idempotency-Key: 3f1c… # optional, see IdempotencyX-Session-Id: ops-room # optional, scopes the agent turnContent-Type: application/json
{ "message": "Summarize today's incidents" }| Header | Required | Meaning |
|---|---|---|
Authorization: Bearer | when pairing is required | Pairing token (see Pairing & authentication) |
X-Webhook-Secret | when a secret is set | Shared secret, compared as a SHA-256 hash in constant time |
X-Idempotency-Key | no | Deduplicates replayed requests within the TTL window |
X-Session-Id | no | Routes the turn into a named session for conversational continuity |
curl -X POST https://<gateway>/webhook \ -H "Authorization: Bearer rk_<token>" \ -H "X-Webhook-Secret: <shared-secret>" \ -H "Content-Type: application/json" \ -d '{"message": "Hello agent"}'WhatsApp webhook
Section titled “WhatsApp webhook”Meta’s WhatsApp Cloud API delivers messages by webhook. The gateway handles both the verification handshake and message delivery.
GET /whatsapp # Meta hub verification challengePOST /whatsapp # inbound message deliveryThe GET handler answers Meta’s subscription handshake using the hub.mode, hub.verify_token, and hub.challenge query parameters: when hub.verify_token matches your configured verify_token, the gateway echoes hub.challenge back. The POST handler verifies the payload HMAC in the X-Hub-Signature-256 header against your app secret, then routes the message to the agent.
Configure the channel under [channels_config.whatsapp]:
[channels_config.whatsapp]access_token = "EAAB…"phone_number_id = "123456789012345"verify_token = "your-verify-token"app_secret = "your-app-secret"allowed_numbers = ["*"]| Field | Meaning |
|---|---|
access_token | Cloud API access token (required) |
phone_number_id | Activates Cloud API mode (required) |
verify_token | Matched against hub.verify_token during the GET handshake (required) |
app_secret | HMAC key for X-Hub-Signature-256 validation; also settable via the REVKA_WHATSAPP_APP_SECRET env var |
allowed_numbers | E.164 numbers allowed to message the agent; ["*"] allows all |
Set the Meta callback URL to https://<gateway>/whatsapp. When no app secret is configured, HMAC verification is skipped (test/dev only — set one for production). For Cloud API vs. WhatsApp Web mode, see WhatsApp (Cloud API & Web).
Linq webhook
Section titled “Linq webhook”Linq’s Partner API delivers iMessage, RCS, and SMS messages by webhook.
POST /linqThe handler verifies an HMAC-SHA256 signature when a signing secret is configured and rejects requests whose timestamp is more than 300 seconds old (replay protection). Self-sent messages (is_from_me / outbound direction) are filtered so the agent does not reply to itself.
[channels_config.linq]api_token = "linq-partner-api-token"from_phone = "+15551234567"signing_secret = "optional-signing-secret"allowed_senders = ["*"]| Field | Meaning |
|---|---|
api_token | Linq Partner API token (required) |
from_phone | E.164 sender number (required) |
signing_secret | HMAC signing secret; overridden by the REVKA_LINQ_SIGNING_SECRET env var |
allowed_senders | E.164 numbers allowed to message the agent; ["*"] allows all |
Set the webhook URL in the Linq dashboard to https://<gateway>/linq. See Email, iMessage, Linq & automation.
WATI webhook
Section titled “WATI webhook”WATI hosts the WhatsApp Business API and pushes messages by webhook.
GET /wati # endpoint verificationPOST /wati # inbound message deliveryInbound messages are filtered through allowed_numbers. Audio attachments are transcribed when [transcription] is enabled, and media downloads are SSRF-guarded by requiring the media host to match the configured api_url host.
[channels_config.wati]api_token = "wati-bearer-token"api_url = "https://live-mt-server.wati.io"tenant_id = "my-tenant"allowed_numbers = ["*"]| Field | Meaning |
|---|---|
api_token | WATI bearer token (required) |
api_url | WATI API base URL (required) |
tenant_id | Optional tenant prefix for recipient targeting |
allowed_numbers | E.164 numbers allowed to message the agent; ["*"] allows all |
See Lark, Feishu, DingTalk, WeCom, QQ & Mochat for WATI alongside the other regional platforms.
Nextcloud Talk webhook
Section titled “Nextcloud Talk webhook”A Nextcloud Talk bot delivers room messages by webhook; the agent replies through the Talk OCS API.
POST /nextcloud-talkWhen a webhook secret is configured, the handler verifies an HMAC-SHA256 signature using the X-Nextcloud-Talk-Random and X-Nextcloud-Talk-Signature headers; invalid or stale requests are rejected with 401. The bot’s own messages are filtered out by bot-name matching to prevent self-echo.
[channels_config.nextcloud_talk]base_url = "https://cloud.example.com"app_token = "nextcloud-talk-app-token"bot_name = "revka"webhook_secret = "optional-webhook-secret"allowed_users = ["*"]| Field | Meaning |
|---|---|
base_url | Nextcloud instance URL (required) |
app_token | Talk bot app token (required) |
bot_name | Display name used for self-echo filtering (default: empty — no name-based self-echo filtering until set) |
webhook_secret | HMAC signing secret; overridden by the REVKA_NEXTCLOUD_TALK_WEBHOOK_SECRET env var |
allowed_users | Nextcloud user IDs allowed to message the agent; ["*"] allows all |
Point the bot’s webhook at https://<gateway>/nextcloud-talk. See Matrix, Mattermost & Nextcloud Talk and Set up Mattermost & Nextcloud Talk.
Gmail Pub/Sub push webhook
Section titled “Gmail Pub/Sub push webhook”Instead of polling IMAP, Gmail can push a notification to Google Cloud Pub/Sub, which forwards it to a webhook. The gateway receives the push and uses the Gmail History API to fetch the new messages.
POST /webhook/gmailThis webhook authenticates differently from the others: when a webhook secret is configured, the request must carry it as a bearer token in the Authorization header (Authorization: Bearer <webhook_secret>) — this is the shared secret you set on the Pub/Sub push subscription, not a pairing token. The endpoint returns 404 when Gmail push is not enabled and 413 when the body exceeds the size limit.
[channels_config.gmail_push]enabled = truetopic = "projects/my-project/topics/gmail-topic"oauth_token = "" # or the GMAIL_PUSH_OAUTH_TOKEN env varlabel_filter = ["INBOX"]allowed_senders = ["*"]webhook_url = "https://<gateway>/webhook/gmail"webhook_secret = "" # or the GMAIL_PUSH_WEBHOOK_SECRET env var| Field | Meaning |
|---|---|
enabled | Must be true to register the channel (default false) |
topic | GCP Pub/Sub topic path (required) |
oauth_token | Gmail API OAuth2 token; falls back to GMAIL_PUSH_OAUTH_TOKEN |
label_filter | Gmail labels to watch (default ["INBOX"]) |
webhook_url | Public URL registered on the Pub/Sub push subscription |
webhook_secret | Bearer secret the push request must present; falls back to GMAIL_PUSH_WEBHOOK_SECRET |
Setup requires a GCP project, a Pub/Sub topic, and granting [email protected] the Pub/Sub Publisher role. The watch subscription auto-renews before its 7-day expiry. Use this to supplement or replace the IMAP email channel for Gmail accounts.
SOP webhook endpoints
Section titled “SOP webhook endpoints”Standard Operating Procedures can be triggered by an inbound webhook. Unlike the channel webhooks above, an SOP webhook trigger is not a separately registered HTTP route — it is a path that the SOP engine matches against an incoming webhook event and dispatches through its locking, audit, and cooldown machinery.
Declare a webhook trigger in the SOP’s SOP.toml:
[[triggers]]type = "webhook"path = "/sop/deploy" # matched exactly against the incoming request path| Field | Meaning |
|---|---|
type | "webhook" for HTTP-triggered SOPs |
path | Exact-match path the incoming webhook event must carry |
When a webhook event matches the path, the engine starts the SOP under its configured execution mode (auto, supervised, step_by_step, priority_based, or deterministic), honoring cooldown_secs and max_concurrent. Multiple SOPs can register the same path — all matching SOPs are dispatched. Every run start is written to the SOP audit log.
Confirm an SOP’s registered triggers before relying on them:
revka sop list # shows each SOP's triggers, e.g. "Triggers: webhook:/sop/deploy, manual"revka sop validate # validate all SOP definitionsRequest signing
Section titled “Request signing”Each webhook uses the signing scheme its platform speaks. The table summarizes how a request proves its authenticity:
| Endpoint | Scheme | Where the secret comes from |
|---|---|---|
POST /webhook | Pairing bearer token, plus optional X-Webhook-Secret (SHA-256, constant-time) | Pairing flow; configured webhook secret |
POST /whatsapp | HMAC-SHA256 in X-Hub-Signature-256 | app_secret or REVKA_WHATSAPP_APP_SECRET |
POST /linq | HMAC-SHA256 + 300 s timestamp freshness | signing_secret or REVKA_LINQ_SIGNING_SECRET |
POST /nextcloud-talk | HMAC-SHA256 over X-Nextcloud-Talk-Random / X-Nextcloud-Talk-Signature | webhook_secret or REVKA_NEXTCLOUD_TALK_WEBHOOK_SECRET |
POST /webhook/gmail | Authorization: Bearer <webhook_secret> (shared secret) | webhook_secret or GMAIL_PUSH_WEBHOOK_SECRET |
GET /whatsapp, GET /wati | Provider hub verification handshake | verify_token (WhatsApp) |
Idempotency
Section titled “Idempotency”Networks and providers retry. The generic /webhook endpoint deduplicates replays with an in-memory idempotency store: send a stable X-Idempotency-Key (for example a UUID) and the gateway runs the agent only the first time it sees that key. A replay within the TTL window returns 200 with a duplicate body and does not re-run the agent:
{ "status": "duplicate", "idempotent": true, "message": "Request already processed for this idempotency key"}Tune the store in gateway config:
[gateway]idempotency_ttl_secs = 300 # how long a key is rememberedidempotency_max_keys = 10000 # bounded cardinality; oldest keys evicted first| Key | Type | Default | Meaning |
|---|---|---|---|
gateway.idempotency_ttl_secs | integer | 300 | Window during which a repeated key is treated as a duplicate |
gateway.idempotency_max_keys | integer | 10000 | Maximum distinct keys retained; eviction is oldest-first |
Rate limiting
Section titled “Rate limiting”The generic /webhook endpoint is protected by the gateway’s per-IP webhook rate limiter (sliding window). The provider webhooks (WhatsApp, Linq, WATI, Nextcloud Talk, Gmail) are NOT rate-limited at the gateway and rely on the provider plus allowlist filtering. The limiter is tunable in config:
[gateway]webhook_rate_limit_per_minute = 60 # 0 disables the webhook rate limitrate_limit_max_keys = 10000 # max tracked IP keys (LRU eviction)trust_forwarded_headers = false # see caution below| Key | Type | Default | Meaning |
|---|---|---|---|
gateway.webhook_rate_limit_per_minute | integer | 60 | Requests per minute per IP; 0 disables |
gateway.rate_limit_max_keys | integer | 10000 | Maximum tracked IP keys before eviction |
gateway.trust_forwarded_headers | boolean | false | Key the limiter on X-Forwarded-For / X-Real-IP instead of the socket peer |
Over-limit requests return 429 Too Many Requests with a retry_after hint.
Channel events ingest
Section titled “Channel events ingest”The gateway also exposes an internal event-ingest endpoint that local services use to broadcast structured events (agent completions, human-approval requests) to connected SSE and WebSocket subscribers, and to fan out notifications to Discord, Slack, or Telegram.
POST /api/channel-events # localhost only — remote IPs receive 403{ "type": "human_approval_request", "content": { "title": "Deploy approval", "message": "Approve production deploy?" }, "channels": ["discord", "slack", "dashboard"], "run_id": "…", "step_id": "…", "approve_keywords": ["approve", "yes"], "reject_keywords": ["reject", "no"]}| Field | Meaning |
|---|---|
type | Event type; "human_approval_request" is handled specially |
channels | Targets to dispatch to: "discord", "slack", "telegram", "dashboard" |
content.title / content.message | Notification text |
run_id / step_id | Required for approval-request events |
approve_keywords / reject_keywords | Keywords that resolve the approval when a reply matches |
For a human_approval_request, the gateway broadcasts the request over SSE and registers it in an approval registry; a Discord or Slack reply matching approve_keywords/reject_keywords then resolves it. This is the inbound counterpart to the workflow approval gate — see Workflows & Architect API and Runs, approvals & checkpoints.
Next steps
Section titled “Next steps”- Connect a messaging channel — end-to-end setup for a webhook-delivered channel.
- Expose your gateway with a tunnel — give providers a public HTTPS URL.
- Realtime: WebSocket, SSE & Live Canvas — subscribe to the events that ingress produces.
- Pairing & authentication — the bearer token the generic webhook can require.
- TLS, rate limiting, WebAuthn & static serving — harden the ingress surface.