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:
- Resolves the binding (registered via
/admin/channels) by URL-id alone — channel providers cannot ship our bearer token. - Verifies the provider's signature scheme before parsing the body so a forged request never reaches the adapter or gateway.
- Translates the provider payload into an
InboundEventand dispatches it through the firstparty gateway, then streams chunks back to the channel. - Returns
200with an empty body on success — anything else triggers the provider's retry loop.
The router lives at packages/channels/webhook_routes.py.
Endpoints
| Method | Path | Auth | Summary |
|---|---|---|---|
| POST | /webhooks/telegram/{binding_id} | Provider signature | Receive 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).
# 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
| Code | When |
|---|---|
200 | Success, or binding is state=DISABLED (paused — accept but don't dispatch), or update kind we don't surface (channel post, edited message, callback query). |
200 | Reaction (message_reaction) translated to a FeedbackEvent and persisted. |
400 | Body is not JSON, or not a JSON object, or binding's protocol is not telegram. |
401 | Binding 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. |
502 | Adapter start() raised — typically a Telegram API outage. Telegram retries. |
503 | ChannelCredentialStore 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_reactionupdate is translated byChannelAdapter.translate_feedbackinto aFeedbackEventand persisted viaFeedbackStore.record. Regular text messages run the on-inbound → gateway → send loop. - Disabled bindings. When the binding's
stateisdisabled, the route returns200without 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 theworkspace_idit 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
200so the provider's delivery loop stays healthy. The underlying serving failure remains visible ininference_log. - Configuration prerequisite. The receiver requires
BIGWAVE_CHANNEL_SECRET_KEYso the credential store can decrypt the binding row. Without it, every delivery returns503. See /admin/channels for the setup steps.