Spaces:
Running
Download docs/webhooks.md from Benaah/MicProtocol: direct link, hf CLI and curl.
- Browser
- Download file 8.53 kB
-
https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/docs/webhooks.md
- Command line
-
hf download hf://spaces/Benaah/MicProtocol/docs/webhooks.md
-
curl -L -o webhooks.md https://huggingface.co/spaces/Benaah/MicProtocol/resolve/main/docs/webhooks.md
Webhooks
MicProtocol receives webhooks from six self-hosted integrations and one realtime SFU. Every webhook is verified, idempotent (via WebhookIdempotencyService), and side-effect-bearing. This doc is the entire HTTP webhook reference.
For the high-level integration architecture, see Third-Party & Self-Hosted Integrations. For the service-client code that calls these integrations outbound, see apps/api/src/integrations/clients/self-hosted-clients.service.ts.
Security Model
Every webhook handler enforces three guarantees:
- HMAC timing-safe signature verification β uses
crypto.timingSafeEqual()so attackers cannot infer the secret via response timing. - Idempotent processing β
WebhookIdempotencyService.generateEventKey()derives a deterministic key from the payload; duplicate deliveries within the TTL window return{ received: true, duplicate: true }and short-circuit side effects. - Workspace-scoped side effects β every state change writes to a record that ultimately chains back to a
WorkspaceviapodcastId, preserving tenant isolation.
The verification helper lives in apps/api/src/integrations/integrations-webhook.controller.ts:
function timingSafeCompare(provided?: string, expected?: string): boolean {
if (!provided || !expected) return false;
const bufProvided = Buffer.from(provided);
const bufExpected = Buffer.from(expected);
if (bufProvided.length !== bufExpected.length) return false;
return crypto.timingSafeEqual(bufProvided, bufExpected);
}
If the matching *_WEBHOOK_SECRET env var is unset (empty string), verification is skipped with a logged warning β useful for first-boot testing but you must set every secret before production.
Endpoint Catalog
All webhooks are mounted under POST /integrations/webhooks/<provider> on the API (:4000 by default).
| Provider | Endpoint | Auth header | Secret env var | Source |
|---|---|---|---|---|
| Cal.com | /integrations/webhooks/calcom |
x-cal-secret-key |
CALCOM_WEBHOOK_SECRET |
Cal.com Admin β Webhooks |
| Chatwoot | /integrations/webhooks/chatwoot/:accountId |
x-chatwoot-signature |
CHATWOOT_WEBHOOK_SECRET |
Chatwoot Settings β Webhooks |
| Documenso | /integrations/webhooks/documenso |
x-documenso-signature |
DOCUMENSO_WEBHOOK_SECRET |
Documenso Admin β Webhooks |
| Formbricks | /integrations/webhooks/formbricks |
x-formbricks-signature |
FORMBRICKS_WEBHOOK_SECRET |
Formbricks Admin β Webhooks |
| Listmonk | /integrations/webhooks/listmonk |
x-listmonk-secret |
LISTMONK_WEBHOOK_SECRET |
Listmonk Settings β Webhooks |
| LiveKit | /integrations/webhooks/livekit |
Authorization (JWT) |
LIVEKIT_API_SECRET |
LiveKit webhook.urls config |
Configure each upstream service to POST to https://<your-api-host>/integrations/webhooks/<provider> with the matching header.
Cal.com
POST /integrations/webhooks/calcom
Auth: x-cal-secret-key header, verified against CALCOM_WEBHOOK_SECRET.
Event types handled (event type is read from body.triggerEvent || body.event):
| Event type | Side effect |
|---|---|
BOOKING_CREATED, BOOKING_CONFIRMED |
Sets matching Booking.isConfirmed = true |
BOOKING_CANCELLED, BOOKING_REJECTED |
Sets matching Booking.isConfirmed = false |
BOOKING_RESCHEDULED |
Updates Booking.startTime and Booking.endTime |
BOOKING_NO_SHOW |
Logged; no schema change (Booking has no no-show field) |
BOOKING_ATTENDEE_ADDED, BOOKING_ATTENDEE_REMOVED |
Logged; future: sync to episode guests |
| Other | Debug-logged |
Booking is resolved by matching the Cal.com UID against Booking.calendarEventId or a substring of Booking.meetLink.
Chatwoot
POST /integrations/webhooks/chatwoot/:accountId
Auth: x-chatwoot-signature header, verified against CHATWOOT_WEBHOOK_SECRET.
:accountId is the Chatwoot account ID (also configurable via CHATWOOT_ACCOUNT_ID env). Multiple accounts supported by routing through the path param.
Event types handled (read from body.event):
| Event | Side effect |
|---|---|
message_created (incoming) |
Logs conversation + contact IDs; calls syncChatwootContact(contactId, accountId) |
conversation_created |
Logs new conversation ID |
conversation_status_changed |
Logs status transition |
contact_created, contact_updated |
Calls syncChatwootContact(contactId, accountId) |
| Other | Debug-logged |
Documenso
POST /integrations/webhooks/documenso
Auth: x-documenso-signature header, verified against DOCUMENSO_WEBHOOK_SECRET.
Patient flow: Documenso fires document_completed or document_declined when all recipients finish. MicProtocol logs the transition keyed by documentId; future work will store signed-doc URLs on the relevant Booking or custom contract model.
Formbricks
POST /integrations/webhooks/formbricks
Auth: x-formbricks-signature header, verified against FORMBRICKS_WEBHOOK_SECRET.
Event type is read from body.event || body.type. If the response payload carries data.userId matching a Guest.id, MicProtocol writes a GuestJourneyEvent:
eventType:follow_uptitle:"Completed Survey"details:"Completed survey <surveyId>"metadata:body.data
This is what powers the "guest completed the post-interview NPS" timeline entry.
Listmonk
POST /integrations/webhooks/listmonk
Auth: x-listmonk-secret header, verified against LISTMONK_WEBHOOK_SECRET.
Confirmed in Listmonk Admin β Settings β Webhooks with the matching secret header (x-listmonk-secret).
Event types handled (read from body.event):
| Event | Side effect |
|---|---|
subscriber.created, subscriber.updated |
Calls syncListmonkSubscriber(subscriber) β upserts Guest.customFields with { listmonkSubscriberId, listmonkStatus, listmonkLists } |
subscriber.deleted |
Calls unsubscribeListmonkSubscriber(email) β stamps Guest.customFields.listmonkUnsubscribedAt |
subscriber.subscribed, subscriber.unsubscribed |
Calls handleListmonkSubscriptionChange(email, lists, event) β logs list membership transitions |
| Other | Debug-logged |
LiveKit
POST /integrations/webhooks/livekit
Auth: Authorization JWT signed with LIVEKIT_API_SECRET. The handler checks for either a valid Authorization header or a body.event payload (defensive β some LiveKit versions send the event in the body without a JWT in dev).
Event types handled (read from body.event):
| Event | Side effect |
|---|---|
room_finished |
Sets matching RecordingSession.status = "completed" and endedAt = now(). Session is resolved by matching body.room.name against RecordingSession.episodeId. |
participant_joined, participant_left |
Logs participant identity + room name |
| Other | Debug-logged |
This is the trigger that flips an episode from "recording" to "recorded" and unlocks the Content Studio domain (see Domain Workflows).
Securing Webhooks in Production
- TLS only β terminate TLS at the API or a reverse proxy; never expose webhooks over plain HTTP.
- Set every
*_WEBHOOK_SECRETβ empty values silently disable verification. Always generate withopenssl rand -hex 32(seescripts/generate-secrets.sh). - Sync secrets on both sides β when you rotate
FORMBRICKS_WEBHOOK_SECRET, update both.envand the Formbricks Admin UI in the same change window. - Restrict by source IP β if your upstream integration publishes source IPs, add an IP allowlist at the network edge (nginx, Cloudflare, security group).
- Idempotency TTL β
WebhookIdempotencyServicekeys live in Redis; confirm your Redis eviction policy isn't aggressively evicting webhook keys during spike traffic (defaultallkeys-lruis safe for typo retries but not for long-delayed redeliveries). - Monitor 401 rates β a spike in
UnauthorizedExceptionfrom the webhook controller means a secret mismatch (rotated on one side only) or an attacker probing. See Operations Runbook β Monitoring. - Log all events β the controller logs every received event at
infolevel. Aggregate these into your APM for a complete "what happened in my integrations" timeline.