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:
| Event | When |
|---|---|
new_visitor | Session created |
chat_ended | Chat finished |
call_ended | Call finished |
booking_created | Booking via any source |
hot_lead_identified | Lead score crossed hot threshold |
form_submitted | Form intercept captured a submission |
lead_score_changed | Score updated (debounced) |
session_finalized | Recording finalized (post-analysis available) |
new_user_identified | Anonymous session became identified (revived event — see below) |
The exact set of live event names is determined by the admin-managed
WebhookEventModelrecords — verify the active list against Settings → Webhooks for a given tenant.
Delivery
Each event:
- Payload JSON-serialized (max body size 1 MB).
- HMAC-SHA256 signature computed over the string
"{timestamp}.{payload}"using the subscription'ssecret. - POST to the subscription's
urlwith headers:X-Webhook-Event: <event_name>X-Webhook-Signature: <hex hmac>X-Webhook-Timestamp: <unix seconds>Content-Type: application/json
- Request times out after 10 seconds.
- 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
inactivemanually 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:
| Field | Notes |
|---|---|
call_sentiment | ElevenLabs analysis sentiment, default neutral |
transcript_text | Formatted Speaker: text lines joined with newlines |
recording_url | Signed public link (see below), or the record's literal URL, '' when unavailable |
- Signed recording links —
RecordingLinkService.buildUrl(): HMAC-SHA256 over<conversationId>.<exp>withRECORDING_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 routeGET /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 fromapi.nextlevelai.site(base rewritten fromapi.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 oninbound_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 toinbound_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):
| Action | Route |
|---|---|
| List subscriptions | GET /webhooks |
| Create subscription | POST /webhooks |
| Get one + 20 recent logs | GET /webhooks/:id |
| Update (url/events/status) | PUT /webhooks/:id |
| Delete (and clean up logs) | DELETE /webhooks/:id |
| Rotate signing secret | POST /webhooks/:id/regenerate-secret |
| Send a test event | POST /webhooks/:id/test |
| Paginated delivery logs | GET /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
2xxwithin 10 seconds, or the delivery is recorded as a failure. - Ordering isn't strict — high-frequency events may arrive out of order; use the
timestampin 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 inapp/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-onlyRecordingLinkServicecopy 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 ownX-Dualhook-SignatureHMAC-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/WebhookEventModelqueries).