MicProtocol / docs /webhooks.md
Benaah's picture
Upload folder using huggingface_hub
b17baae verified
|
Raw History Blame Contribute Delete
8.53 kB

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:

  1. HMAC timing-safe signature verification β€” uses crypto.timingSafeEqual() so attackers cannot infer the secret via response timing.
  2. 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.
  3. Workspace-scoped side effects β€” every state change writes to a record that ultimately chains back to a Workspace via podcastId, 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_up
  • title: "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

  1. TLS only β€” terminate TLS at the API or a reverse proxy; never expose webhooks over plain HTTP.
  2. Set every *_WEBHOOK_SECRET β€” empty values silently disable verification. Always generate with openssl rand -hex 32 (see scripts/generate-secrets.sh).
  3. Sync secrets on both sides β€” when you rotate FORMBRICKS_WEBHOOK_SECRET, update both .env and the Formbricks Admin UI in the same change window.
  4. Restrict by source IP β€” if your upstream integration publishes source IPs, add an IP allowlist at the network edge (nginx, Cloudflare, security group).
  5. Idempotency TTL β€” WebhookIdempotencyService keys live in Redis; confirm your Redis eviction policy isn't aggressively evicting webhook keys during spike traffic (default allkeys-lru is safe for typo retries but not for long-delayed redeliveries).
  6. Monitor 401 rates β€” a spike in UnauthorizedException from the webhook controller means a secret mismatch (rotated on one side only) or an attacker probing. See Operations Runbook β†’ Monitoring.
  7. Log all events β€” the controller logs every received event at info level. Aggregate these into your APM for a complete "what happened in my integrations" timeline.