Skip to main content

Webhooks

Inbound webhooks are how external services reach Mesachat — Telegram delivers messages, Mailgun delivers email, Clerk delivers identity events.

Derived from backend/src/api/routes/webhooks.ts and backend/src/auth/clerk-webhook-handler.ts.

Webhooks are not under /api

Every webhook path is mounted at the server root, not beneath /api:

/webhooks/telegram/:botId
/webhooks/email/:botId
/webhooks/email
/webhooks/clerk
/webhooks/health
/api/webhooks/… is not a route.

If you configured a webhook at https://<host>/api/webhooks/clerk, deliveries have been 404ing and nothing has been syncing. The correct path is https://<host>/webhooks/clerk. Earlier versions of this page — and of the internal setup guide — gave the /api prefix. They were wrong.

The origin can differ from your app origin. Deployments that keep the app on a private network still have to accept deliveries from the public internet, so webhooks are commonly published on a separate, public hostname. The value of WEBHOOK_BASE_URL is what Mesachat tells each platform to call, and it is what those platforms will use.

Authentication

Webhook routes sit outside the /api authentication gate. They are not exempt from verification — each handler verifies the sender itself, by a different mechanism per platform. The three mechanisms do not behave alike, and the differences matter for production.

SourceMechanismWith no secret configured
TelegramX-Telegram-Bot-Api-Secret-Token, compared in constant timeRejected in production; accepted otherwise
ClerkSvix signature over the raw bodyRejected — the handler throws
Mailgun (email)HMAC over timestamp + token⚠️ Accepted — verification is skipped

Rate limit

All /webhooks traffic is limited to 600 requests per client IP per minute, applied before signature verification so forged or replayed deliveries cannot drive database lookups and bot cold-starts. Exceeding it returns 429. The limiter is skipped when NODE_ENV=test.

Telegram

POST /webhooks/telegram/:botId
  1. Creating a Telegram integration registers this URL with Telegram's Bot API.
  2. Telegram POSTs updates to it.
  3. Mesachat routes the message to the bot's configured agents.

Securing it

Set webhookSecretToken under the integration's settings.telegram. Telegram then sends it in the X-Telegram-Bot-Api-Secret-Token header on every delivery, and Mesachat compares it in constant time.

In production, an integration with no secret token is rejectedverifyTelegramWebhook returns false when no secret is configured and NODE_ENV=production. Outside production the unsigned path stays open so you can test locally. So: a production bot without a secret token does not run insecurely, it stops receiving messages.

Registration is automatic

EventAction
Integration createdRegisters the webhook with Telegram
Integration updatedRe-registers if the URL changed
Integration deletedRemoves the webhook from Telegram

Registration calls getWebhookInfo() before setWebhook() so an unchanged URL is not re-registered, and retries with exponential backoff when Telegram answers 429.

Email

POST /webhooks/email/:botId # routed to a known bot
POST /webhooks/email # catch-all, bot resolved from the payload

Mailgun posts inbound mail as multipart/form-data. Uploads are capped at 25 MB per file, 10 files per request.

Securing it — do not skip this

Set webhookSigningKey under the integration's settings.email.

With no signing key configured, signature verification is bypassed and the payload is

accepted — in every environment, including production. The bypass is logged at SECURITY severity (email.webhook.signature.BYPASSED), but it does not fail the request. Unlike the Telegram path, this one does not fail closed.

An email integration deployed without settings.email.webhookSigningKey will act on forged inbound mail. Set the key before you route real mail to it, and grep your logs for email.webhook.signature.BYPASSED to confirm no integration is running without one.

When a key is present, Mesachat verifies Mailgun's timestamp + token + signature triple, and does so before loading or starting the bot — so an unverified payload cannot trigger a cold-start.

Clerk

POST /webhooks/clerk

Syncs identity from Clerk into Mesachat.

EventAction
user.created · user.updated · user.deletedCreate, update, remove the Mesachat user
organization.created · updated · deletedCreate, update, remove the org-to-tenant mapping
organizationMembership.created · updated · deletedSync membership and roles

Setup

  1. Clerk Dashboard → WebhooksAdd Endpoint.
  2. URL: https://<your-webhook-host>/webhooks/clerkno /api prefix.
  3. Subscribe to the nine events above.
  4. Copy the signing secret into CLERK_WEBHOOK_SECRET.

Step 4 is not optional. With CLERK_WEBHOOK_SECRET unset the handler throws on every delivery, so identity sync fails closed rather than accepting unverified events. Verification uses Svix over the raw request body.

Health

GET /webhooks/health

Reports the webhook subsystem's status. Unauthenticated, like the rest of /webhooks.

Outbound webhooks

Mesachat does not send webhooks to your systems. Everything here is inbound.