data block is documented.
Call lifecycle
call.started
Fires when the dialer originates a call (pre-answer).
call.answered
Fires on CHANNEL_ANSWER — the callee picked up.
call.ended
Fires on CHANNEL_HANGUP, for every call, once the call and lead rows have
been finalised — so a GET /v1/calls/{id} issued the moment you receive the
event already returns the same durations and disposition.
call.ended carries no phone number and no lead name. Join on
lead_id (or salesforce_lead_id) against your own record if you need
the contact details — this keeps the event safe to log and safe to
deliver for tenants under PII-strict handling.recording.available is usually false at hangup — the upload to storage has
not finished yet. call.recording_ready follows 30–120 s later with the same
recording.url, which is stable and safe to persist: it re-signs storage on
every fetch instead of handing you a URL that expires.
call.disposition_set
Fires when a call’s behavioural outcome is recorded — Interested, Site
Visit, Do Not Call. call.ended carries the mechanical outcome (answered,
busy, no answer); this carries what the call was actually worth, and it is
usually the event a CRM wants.
It fires twice in two different situations, and previous_disposition
tells them apart:
Re-saving the same value is not a transition and publishes nothing, so a
double-clicked Save or a retried AI verb will not produce a duplicate.
call.disposition_set carries no phone number, no lead name, and no
agent notes. Join on lead_id (or salesforce_lead_id) against your own
record if you need contact details. Notes are excluded deliberately: free
text an agent typed is the one field here where a number can arrive without
any field being named phone.call.recording_ready
Fires when the recording is uploaded and available for playback — typically
30–120 s after call.ended.
recording.url is a non-expiring, HMAC-tokenised public link — it
never rotates or expires and needs no API key, so it’s the field to
persist in external systems: a Salesforce Call_Detail__c.Recording_File__c,
a CRM attachment, a webhook subscriber’s own database. The legacy
top-level recording_url is the tenant-scoped, API-key-gated audio proxy
(also stable, but requires your API credentials on every fetch — use it
from server-side code that already holds them). signed_url is a direct
GCS link that expires after 1 hour; use it only for an immediate
fetch, never for storage — persist recording.url or recording_url
instead.call.transcript_ready
Fires when Gemini transcription completes (typically +30–60s after
recording upload).
Lead lifecycle
lead.completed
Fires when a lead reaches a terminal state (completed, failed,
dnd, or no_answer after exhausted retries).
Campaign lifecycle
campaign.started
campaign.paused
Fires on BOTH manual and auto-pause. Distinguish via data.source:
campaign.resumed
Symmetric to paused. source is "manual" in all current paths
(auto-resume from safety-sweep doesn’t exist — humans re-enable).
campaign.completed
reason is "stopped" when an operator or an API call stopped it, and
"leads_finished" when it
stopped by itself
because all its leads were finished. previous_status is running or
paused.
campaign.reopened
Fires when a stopped campaign is restarted (completed → running) — the
usual reason being leads imported after the stop. A campaign that stopped by
itself (reason: "leads_finished") restarts by itself when new leads arrive,
and fires this too. Emitted alongside
campaign.started, never instead of it, so subscribing to this is only
necessary if you treated campaign.completed as final.
completed_at is the timestamp the campaign carried before the restart
cleared it, or null if it never had one.

