Skip to main content

Webhooks

Audience: Engineering, support, management · Where in app: Settings → Webhooks · Plan availability: All plans (verify)

When Zapier isn't enough — because the customer wants to hit their own API, needs signed/verifiable payloads, or wants maximum throughput — native webhooks send events straight to any HTTP endpoint. Each delivery is HMAC-signed so the receiver can prove it came from Knock Knock, and every attempt is logged for auditing.

What it does

  • Subscribes one or more endpoints to Knock Knock events.
  • Delivers a JSON payload per event, signed with HMAC-SHA256.
  • Logs every delivery attempt (status code, response body, latency) for 30 days.
  • Lets admins create their own event types and send test events.

How it works

Auth model: customer-supplied URL + per-subscription signing secret. No OAuth — the customer registers an endpoint URL and Knock Knock generates a signing secret. Direction: outbound only.

Subscriptions (WebhookModel)

Each subscription row:

{
company_id, // owning tenant
url, // destination endpoint
description, // optional label
events: [...], // event names this endpoint receives
secret, // HMAC signing secret (32-byte hex; hidden by default)
status // "active" | "inactive"
}

Multiple subscriptions per company are allowed — different URLs subscribing to different events.

Event types (WebhookEventModel)

Event types are admin-defined records, not a fixed hard-coded list. Each has a name, label, description, and a sample_payload. Non-admins see only active event types. The product surfaces a working set of events:

EventWhen
new_visitorSession created
chat_endedChat finished
call_endedCall finished
booking_createdBooking via any source
hot_lead_identifiedLead score crossed hot threshold
form_submittedForm intercept captured a submission
lead_score_changedScore updated (debounced)
session_finalizedRecording finalized (post-analysis available)
new_user_identifiedAnonymous session became identified (revived event — see below)

The exact set of live event names is determined by the admin-managed WebhookEventModel records — verify the active list against Settings → Webhooks for a given tenant.

Delivery

Each event:

  1. Payload JSON-serialized (max body size 1 MB).
  2. HMAC-SHA256 signature computed over the string "{timestamp}.{payload}" using the subscription's secret.
  3. POST to the subscription's url with headers:
    • X-Webhook-Event: <event_name>
    • X-Webhook-Signature: <hex hmac>
    • X-Webhook-Timestamp: <unix seconds>
    • Content-Type: application/json
  4. Request times out after 10 seconds.
  5. Logged in WebhookLogModel (success/failure, response status, response body, duration_ms, error). Logs auto-expire after 30 days (TTL).

The delivered payload wraps the event data:

{
"event": "booking_created",
"data": { "...event-specific..." },
"webhook_id": "...",
"company_id": "...",
"timestamp": "1716200000"
}

Retry behavior — verify. In the current backend code, delivery is a single attempt (logged on failure, not automatically retried), and subscriptions are only marked inactive manually by an admin. The knowledge base describes exponential backoff (3 retries) and auto-deactivation after 3 consecutive failures — treat that as roadmap/aspirational until confirmed in code. For production reliability, recipients should be resilient and admins should monitor the delivery log.

Post-call payload & delivery (call_ended)

The post-call payload (PostCallWebhookService) now also carries:

FieldNotes
call_sentimentElevenLabs analysis sentiment, default neutral
transcript_textFormatted Speaker: text lines joined with newlines
recording_urlSigned public link (see below), or the record's literal URL, '' when unavailable
  • Signed recording linksRecordingLinkService.buildUrl(): HMAC-SHA256 over <conversationId>.<exp> with RECORDING_LINK_SECRET, TTL 365 days (links get stored on CRM contacts and clicked weeks later; revocable by rotating the secret). Verified constant-time by the public route GET /public/recordings/:id?exp&token, which streams from ElevenLabs on demand — audio is never stored. Fail-closed: no secret / no base → no link, never an unsigned one. Links are served from api.nextlevelai.site (base rewritten from api.knock-knockapp.com). ms-communication has a sign-only copy (ms-communication/app/services/RecordingLinkService.js) — the signing input and secret must stay identical in both.
  • Delivery timing / exactly-once — ms-communication (InboundOutboundCallController.saveAndEmail) deliberately holds the webhook until the ElevenLabs post-call analysis lands (it carries summary/recording/sentiment). If the analysis never arrives, PostCallWebhookFallbackJob (backend cron, every minute, 5-minute grace) fires with whatever exists. Both paths share an atomic claim on inbound_outbound_calls.post_call_webhook_sent_at, so a late analysis and the fallback can never both send. This is the one place backend writes to inbound_outbound_calls.

new_user_identified (revived)

Formerly fired only from the (now dead) HappierLeads inbound handler; now emitted from session create (UserSessionController) — at most once per session, and only when the visitor arrived anonymous and got identified during that session (never on a pageview of an already-known person). Source precedence: KnockKnock's own resolved person (contact.source: "knockknock") outranks Apollo's guessed employee ("apollo"). Payload: user_session_id, contact {name, email, phone, source}, company, ip, initial_page_visit, identified_at.

Verification (recipient side)

The receiver recomputes the HMAC over "{timestamp}.{body}" with the shared secret and compares (constant-time):

import hmac, hashlib
ts = request.headers["X-Webhook-Timestamp"]
sig = request.headers["X-Webhook-Signature"]
mac = hmac.new(secret.encode(), f"{ts}.{request.body}".encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, mac):
return 403

Configuration & options

Routes (all under the authenticated admin app):

ActionRoute
List subscriptionsGET /webhooks
Create subscriptionPOST /webhooks
Get one + 20 recent logsGET /webhooks/:id
Update (url/events/status)PUT /webhooks/:id
Delete (and clean up logs)DELETE /webhooks/:id
Rotate signing secretPOST /webhooks/:id/regenerate-secret
Send a test eventPOST /webhooks/:id/test
Paginated delivery logsGET /webhooks/:id/logs
Manage event types (admin)GET/POST/PUT/DELETE /webhook-events

Behaviors & edge cases

  • Webhook Events screen shows recent deliveries: event, URL, status, response code, response-body excerpt, latency, retry count; filter by event/success/date.
  • Timeout: the recipient must respond 2xx within 10 seconds, or the delivery is recorded as a failure.
  • Ordering isn't strict — high-frequency events may arrive out of order; use the timestamp in the payload.
  • No bulk historical replay — re-delivery is per-event (via the events screen) where supported; there's no backfill of past events.

Plan & limits

  • Available on all plans (verify tier gating).
  • 1 MB max payload; 10s delivery timeout; 30-day log retention.

Technical implementation

  • Owning service: backend.
  • Models: app/models/WebhookModel.js (subscriptions), WebhookEventModel.js (event-type catalog), WebhookLogModel.js (delivery log, 30-day TTL).
  • Controllers/routes: app/controllers/WebhookController.js, WebhookEventController.js; app/routes/webhookRoutes.js, webhookEventRoutes.js. Signing in app/helpers/WebhookHelper.js (crypto.createHmac('sha256', secret) over "{timestamp}.{payload}").
  • Post-call path: app/services/PostCallWebhookService.js, app/services/RecordingLinkService.js, app/jobs/PostCallWebhookFallbackJob.js (+ the sign-only RecordingLinkService copy in ms-communication).
  • See CRM & External Integration.

Note: the WhatsApp onboarding path (Dualhook) is a separate inbound webhook layer (DualhookService.js / DualhookWebhookController.js) with its own X-Dualhook-Signature HMAC-SHA256 verification — not part of the customer-facing outbound webhooks described here.

What Nox can tell you

  • Which webhooks are subscribed.
  • Recent failures (count + URLs) and average latency per subscription.
  • Whether a specific event got delivered (via WebhookLogModel / WebhookEventModel queries).