Spaces:
Running
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
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](./third-party-integrations.md). For the service-client code that calls these integrations outbound, see [`apps/api/src/integrations/clients/self-hosted-clients.service.ts`](../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`](../apps/api/src/integrations/integrations-webhook.controller.ts): | |
| ```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](./workflows.md)). | |
| ## 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`](../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](./operations-runbook.md#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. | |