Skip to content

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.

The simplest integration path. POST a JSON message and the gateway runs it as an agent turn.

POST /webhook
Content-Type: application/json
{ "message": "Hello agent" }

The message field is required; any other JSON shape returns 400. The response carries the agent’s reply.

The generic webhook is gated by two independent, stackable layers:

  1. Pairing bearer token. When [gateway].require_pairing = true (the default), /webhook requires the same Authorization: Bearer rk_<token> as the rest of the API. An unpaired or invalid token returns 401. Set require_pairing = false only for a gateway bound to localhost on a trusted machine.
  2. Webhook secret (optional). If a webhook secret is configured, the caller must also send it in the X-Webhook-Secret header. 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 returns 401.
POST /webhook
Authorization: Bearer rk_<token>
X-Webhook-Secret: <shared-secret>
X-Idempotency-Key: 3f1c… # optional, see Idempotency
X-Session-Id: ops-room # optional, scopes the agent turn
Content-Type: application/json
{ "message": "Summarize today's incidents" }
HeaderRequiredMeaning
Authorization: Bearerwhen pairing is requiredPairing token (see Pairing & authentication)
X-Webhook-Secretwhen a secret is setShared secret, compared as a SHA-256 hash in constant time
X-Idempotency-KeynoDeduplicates replayed requests within the TTL window
X-Session-IdnoRoutes the turn into a named session for conversational continuity
Terminal window
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"}'

Meta’s WhatsApp Cloud API delivers messages by webhook. The gateway handles both the verification handshake and message delivery.

GET /whatsapp # Meta hub verification challenge
POST /whatsapp # inbound message delivery

The 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 = ["*"]
FieldMeaning
access_tokenCloud API access token (required)
phone_number_idActivates Cloud API mode (required)
verify_tokenMatched against hub.verify_token during the GET handshake (required)
app_secretHMAC key for X-Hub-Signature-256 validation; also settable via the REVKA_WHATSAPP_APP_SECRET env var
allowed_numbersE.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’s Partner API delivers iMessage, RCS, and SMS messages by webhook.

POST /linq

The 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 = ["*"]
FieldMeaning
api_tokenLinq Partner API token (required)
from_phoneE.164 sender number (required)
signing_secretHMAC signing secret; overridden by the REVKA_LINQ_SIGNING_SECRET env var
allowed_sendersE.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 hosts the WhatsApp Business API and pushes messages by webhook.

GET /wati # endpoint verification
POST /wati # inbound message delivery

Inbound 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 = ["*"]
FieldMeaning
api_tokenWATI bearer token (required)
api_urlWATI API base URL (required)
tenant_idOptional tenant prefix for recipient targeting
allowed_numbersE.164 numbers allowed to message the agent; ["*"] allows all

See Lark, Feishu, DingTalk, WeCom, QQ & Mochat for WATI alongside the other regional platforms.

A Nextcloud Talk bot delivers room messages by webhook; the agent replies through the Talk OCS API.

POST /nextcloud-talk

When 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 = ["*"]
FieldMeaning
base_urlNextcloud instance URL (required)
app_tokenTalk bot app token (required)
bot_nameDisplay name used for self-echo filtering (default: empty — no name-based self-echo filtering until set)
webhook_secretHMAC signing secret; overridden by the REVKA_NEXTCLOUD_TALK_WEBHOOK_SECRET env var
allowed_usersNextcloud 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.

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/gmail

This 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 = true
topic = "projects/my-project/topics/gmail-topic"
oauth_token = "" # or the GMAIL_PUSH_OAUTH_TOKEN env var
label_filter = ["INBOX"]
allowed_senders = ["*"]
webhook_url = "https://<gateway>/webhook/gmail"
webhook_secret = "" # or the GMAIL_PUSH_WEBHOOK_SECRET env var
FieldMeaning
enabledMust be true to register the channel (default false)
topicGCP Pub/Sub topic path (required)
oauth_tokenGmail API OAuth2 token; falls back to GMAIL_PUSH_OAUTH_TOKEN
label_filterGmail labels to watch (default ["INBOX"])
webhook_urlPublic URL registered on the Pub/Sub push subscription
webhook_secretBearer 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.

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
FieldMeaning
type"webhook" for HTTP-triggered SOPs
pathExact-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:

Terminal window
revka sop list # shows each SOP's triggers, e.g. "Triggers: webhook:/sop/deploy, manual"
revka sop validate # validate all SOP definitions

Each webhook uses the signing scheme its platform speaks. The table summarizes how a request proves its authenticity:

EndpointSchemeWhere the secret comes from
POST /webhookPairing bearer token, plus optional X-Webhook-Secret (SHA-256, constant-time)Pairing flow; configured webhook secret
POST /whatsappHMAC-SHA256 in X-Hub-Signature-256app_secret or REVKA_WHATSAPP_APP_SECRET
POST /linqHMAC-SHA256 + 300 s timestamp freshnesssigning_secret or REVKA_LINQ_SIGNING_SECRET
POST /nextcloud-talkHMAC-SHA256 over X-Nextcloud-Talk-Random / X-Nextcloud-Talk-Signaturewebhook_secret or REVKA_NEXTCLOUD_TALK_WEBHOOK_SECRET
POST /webhook/gmailAuthorization: Bearer <webhook_secret> (shared secret)webhook_secret or GMAIL_PUSH_WEBHOOK_SECRET
GET /whatsapp, GET /watiProvider hub verification handshakeverify_token (WhatsApp)

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 remembered
idempotency_max_keys = 10000 # bounded cardinality; oldest keys evicted first
KeyTypeDefaultMeaning
gateway.idempotency_ttl_secsinteger300Window during which a repeated key is treated as a duplicate
gateway.idempotency_max_keysinteger10000Maximum distinct keys retained; eviction is oldest-first

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 limit
rate_limit_max_keys = 10000 # max tracked IP keys (LRU eviction)
trust_forwarded_headers = false # see caution below
KeyTypeDefaultMeaning
gateway.webhook_rate_limit_per_minuteinteger60Requests per minute per IP; 0 disables
gateway.rate_limit_max_keysinteger10000Maximum tracked IP keys before eviction
gateway.trust_forwarded_headersbooleanfalseKey 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.

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"]
}
FieldMeaning
typeEvent type; "human_approval_request" is handled specially
channelsTargets to dispatch to: "discord", "slack", "telegram", "dashboard"
content.title / content.messageNotification text
run_id / step_idRequired for approval-request events
approve_keywords / reject_keywordsKeywords 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.