Skip to main content

Webhooks

Webhooks are the primary channel for results; polling GET /v1/calls/{call_id} is the fallback. An administrator registers the endpoint on the agent: open the agent in the editor → Settings tab → section Webhooks (post-call). The payload contents are also described on the Webhooks page.
  • URLhttp:// or https:// (the platform does not enforce HTTPS; use it anyway). Delivery is subject to the platform’s outbound host policy: private addresses and disallowed hosts may be refused by the operator’s configuration, and such deliveries fail into the dead-letter queue.
  • Event subscription — any subset of the three events below (default: call_ended + call_analyzed).
  • Signing secret — configured on the platform side as a reference to a server-side secret (the name of an environment variable of the platform deployment); the value itself is never entered in the dashboard and is shared with you out of band.
  • Agent — an endpoint is bound to exactly one live agent of the organization. The field is mandatory: an empty agent name is refused with webhooks.agent_required, an unknown or archived one with webhooks.agent_not_found. There is no organization-wide level — to cover several agents, register the endpoint on each of them (the same URL may be reused; agent_id in the payload tells them apart).
  • Test events — an opt-in flag makes the endpoint also receive events of editor test sessions (mode: "test").
  • Timeout — how long the platform waits for your 2xx (platform default 10 s, configurable per endpoint up to 120 s; 0 means the platform default).

Event envelope

Every event is an HTTP(S) POST with a JSON body (UTF-8, non-ASCII characters unescaped). Headers on every delivery:

Which event arrives when

  • call_ended — as soon as the session finishes, before analysis and usually before the recording is finalized. This is the early notification: transcript, call_status, status_reason, error_code, and the timing fields (answered_at, media_started_at, talk_from, talk_to). latency is present only when per-turn metrics were collected; its keys are the raw metric names ending in _ms (turn_ms, llm_ms, asr_ms, tts_ms, …) with values {avg, max} in milliseconds.
  • call_analyzed — when both post-call pipelines are done: the analysis (terminal in any of done, skipped, failed, interrupted — the event is always delivered) and the recording finalization. The event is self-contained: full call block + analysis + ready recording. If you only need the final outcome, subscribe to call_analyzed alone and skip call_ended. Its call block is identical to call_ended.call except that the recording stays top-level and is not duplicated inside call.
  • observer_incidentduring the call, when a compliance observer configured on the agent detects a violation and its reaction includes a notification. Use it for real-time supervisor alerts. severity is low | medium | high (an unknown value is normalized to medium), confidence is 0–1 or null, mode may be "" when unknown. Multiple incidents in one call produce separate events; retries of the same incident keep the same event_id.
  • ping — a test delivery from the dashboard button; agent_id is the agent the endpoint is bound to ("" for an organization-level endpoint). Respond 2xx and verify its signature like a real event. It is not retried.
When analysis_status is anything but done, call_analysis is {} and a top-level reason field explains why (for example voicemail, no_speech, analytics_disabled); analytics_tier, analytics_model_used and analytics_language are empty strings then.

Delivery semantics

  • At-least-once. A delivery counts as successful only on a 2xx from your endpoint; anything else (non-2xx, timeout, connection error) is retried. You will receive duplicates — deduplicate on event_id.
  • Retries with exponential backoff — platform settings, not per endpoint: by default up to 8 attempts with pauses of 30, 60, 120, 240, 480, 960 and 1920 seconds. After the final failed attempt the event moves to a dead-letter queue, from which an administrator can redeliver it manually (History → session details → webhook deliveries). Dead deliveries are purged after a retention period, so the manual-retry window is finite.
  • Deliveries always go to the endpoint’s current URL, so a mistyped URL can be corrected and the event redelivered; an endpoint that was deleted or disabled sends its pending events straight to the dead-letter queue.
  • Ordering is best-effort. call_ended is enqueued before call_analyzed, but delivery order is not guaranteed; observer_incident arrives mid-call. Treat each event independently, correlating by call_id.
  • Respond fast. Return 2xx immediately after signature verification and process asynchronously: the platform waits at most the endpoint’s timeout.

Signature verification

X-CFlow-Timestamp is an integer number of Unix seconds. Verify on the raw bytes of the body (before any JSON parsing or re-serialization — the body is UTF-8 with unescaped non-ASCII characters), compare in constant time, and reject stale timestamps — a 300-second window. Retried deliveries are re-signed with a fresh timestamp, so the window does not conflict with retries.
In Python (Flask) use request.get_data(); in Node (Express) use express.raw({ type: "application/json" }) — both preserve the exact bytes the signature was computed over.

Recordings

recording_url is a stable URL (https://{{HUBTALK_FQDN}}/api/recordings/rec-…): it does not expire and can be persisted in your systems. Each GET to it answers 307 with a fresh short-lived download link, so a URL saved months ago keeps working — until the recording itself is deleted by the retention policy (recording.expires_at). Other answers: 409 while the recording is not ready yet (keep polling), 410 after deletion, 503 when the storage is temporarily unavailable. Append ?download=1 for an attachment disposition. The URL is unauthenticated but unguessable; treat it as a capability token. One edge case: if the recording takes unusually long to finalize (longer than the platform’s wait, 30 s by default), call_analyzed is still emitted — with the current non-terminal recording status and an empty recording_url. The stable URL starts working on its own once the recording is ready.