Skip to content

Webhooks ​

Inbound webhook receivers that channel providers (Telegram, later Slack and Discord) call into when a user sends a message in the external chat client. Each receiver:

  1. Resolves the binding (registered via /admin/channels) by URL-id alone — channel providers cannot ship our bearer token.
  2. Verifies the provider's signature scheme before parsing the body so a forged request never reaches the adapter or gateway.
  3. Translates the provider payload into an InboundEvent and dispatches it through the firstparty gateway, then streams chunks back to the channel.
  4. Returns 200 with an empty body on success — anything else triggers the provider's retry loop.

The router lives at packages/channels/webhook_routes.py.

Endpoints ​

MethodPathAuthSummary
POST/webhooks/telegram/{binding_id}Provider signatureReceive a Telegram Update for binding_id.

Slack and Discord receivers

Slack and Discord channel adapters are not yet shipped; only the Telegram receiver ships in this release. ChannelProtocol.SLACK and ChannelProtocol.DISCORD are reserved enum members but no webhook routes are mounted for them yet.

POST /webhooks/telegram/{binding_id} ​

Telegram delivers updates as JSON POST bodies. When the operator configures setWebhook(secret_token=...) Telegram echoes the token in X-Telegram-Bot-Api-Secret-Token; the route verifies this against the binding's webhook_secret (rotated via POST /admin/channels/{id}:rotate).

Successful delivery returns 200 within Telegram's ~60 s budget. Anything else triggers retry.

The body is the raw Telegram Update JSON — see Telegram's Bot API for the shape. The route does not echo or transform the body in the response (Telegram's success contract is just 200).

bash
# Sent by Telegram, not by an operator — shown for shape only.
curl -X POST \
  -H "Content-Type: application/json" \
  -H "X-Telegram-Bot-Api-Secret-Token: <webhook_secret>" \
  -d '{"update_id":1,"message":{"chat":{"id":1},"from":{"id":2},"text":"hi"}}' \
  https://your-bigwave.example.com/webhooks/telegram/<binding_id>

Status codes ​

CodeWhen
200Success, or binding is state=DISABLED (paused — accept but don't dispatch), or update kind we don't surface (channel post, edited message, callback query).
200Reaction (message_reaction) translated to a FeedbackEvent and persisted.
400Body is not JSON, or not a JSON object, or binding's protocol is not telegram.
401Binding not found, or signature mismatch — returned even on the not-found path so an attacker gets the same response as a legitimately mis-routed delivery.
502Adapter start() raised — typically a Telegram API outage. Telegram retries.
503ChannelCredentialStore not initialised on app.state (lifespan incomplete).

Operator notes ​

  • No bearer auth. This is the only HTTP surface in the tree whose request is not authenticated by BIGWAVE_ADMIN_TOKEN. The auth contract is the binding-id-as-URL pattern plus the provider's signature. Treat the URL as a secret — leaking it costs you the workspace's chat traffic but, by itself, cannot forge messages because the signature still has to match.
  • Reactions vs messages. A Telegram message_reaction update is translated by ChannelAdapter.translate_feedback into a FeedbackEvent and persisted via FeedbackStore.record. Regular text messages run the on-inbound → gateway → send loop.
  • Disabled bindings. When the binding's state is disabled, the route returns 200 without dispatching — this prevents Telegram from accumulating retries on a deliberately paused channel.
  • Workspace fence. The webhook route loads the binding unscoped (the only place in the tree that does so on purpose, see _get_binding_unscoped). The binding row carries the workspace_id it was created with; downstream gateway dispatch is scoped via that field, never via a request-time principal.
  • Failure surface. Send-side failures to the channel are swallowed and logged — the receiver still returns 200 so the provider's delivery loop stays healthy. The underlying serving failure remains visible in inference_log.
  • Configuration prerequisite. The receiver requires BIGWAVE_CHANNEL_SECRET_KEY so the credential store can decrypt the binding row. Without it, every delivery returns 503. See /admin/channels for the setup steps.

Dual-licensed: MIT OR Apache-2.0