Skip to main content

Webhook Events Reference

Payload shape for every event the platform emits. The names and samples on this page mirror what the dispatcher actually sends — generated from WEBHOOK_EVENT_TYPES in packages/shared/src/constants.ts and the dispatchTenantWebhook / buildWebhookJobs call sites. For the full catalog with one-liner descriptions, see Webhook Events.

Event Envelope

Every webhook delivery wraps the event in a standard envelope:
When truncated: true, data carries _truncated: true, _original_bytes, and a small set of identifier fields (id, message_id, conversation_id, contact_id, campaign_id, channel) so consumers can locate the resource.

Catalog Index

Every webhook event Orbit emits — 249 in total — grouped by surface. Each row links to its long-form payload section below; combined headings (e.g. campaign create / update / delete share one section) point to the first member.

Message Events

Contact Events

Campaign Events

Voice Events

Agent Events

Flow Events

Verification Events

Number / Porting Events

Conversation Events

Compliance Events

List / Segment Events

Webhook Endpoint Events

Billing Events

Commerce Events

Channel Failover Events

Push Events

Number Masking (Proxy) Events

WhatsApp Events

Supervisor Events

SIP Trunk Events

CDP Events

Other


Message Events

Every message lifecycle event (message.sent, message.delivered, message.failed, message.read, plus the submitted_no_receipt and expired transitions) carries a state_class field alongside status. It tags the transition as either "intermediate" (the status may still progress — e.g. submitted_no_receipt, where a later genuine delivered can land) or "terminal" (a final outcome such as delivered / failed / read), so per-message status_callback subscribers can distinguish the two without re-deriving the status taxonomy.Each lifecycle payload also carries is_terminal (boolean) — a convenience discriminator that is always equivalent to state_class === "terminal". Both derive from the same terminal-status allow-list and can never disagree, so high-volume senders can branch on a single truthy flag (if (dlr.is_terminal)) instead of string-comparing the enum. Like state_class, is_terminal is present only on lifecycle transitions (where status is defined). Engagement events (email.opened / email.clicked) are not lifecycle transitions and intentionally omit both state_class and is_terminal.

message.created

Fired when an outbound message is accepted and queued for delivery.

message.sent

Fired when the message is handed off to the carrier / provider.

message.delivered

message.failed

Terminal failure reported by the carrier (or a synthesised failure covering undelivered, rejected, expired, and submitted_no_receipt — all of which STATUS_TO_EVENT fans out on this event; branch on the payload’s status field to tell them apart).
error_code and error_message are optional — they appear only when the message carries a provider-supplied failure reason. They mirror the raw code and human-readable text the carrier / channel returned (for example a WhatsApp Cloud API errors[0].code / .title, or an SMS / email carrier failure), and are the same error_code / error_message values the REST message object and SDK expose. Both are strings. A message.failed payload for a message with no captured provider error omits both keys, so treat them as may-be-absent rather than null. Successful transitions (message.sent, message.delivered, message.read) clear any prior failure reason, so those events never carry these two fields. The expired and submitted_no_receipt transitions (also delivered via message.failed, below) follow the same rule.

message.read

message.received

Inbound message from a recipient.

expired and submitted_no_receipt (delivered via message.failed)

These two terminal-non-success transitions do not dispatch dedicated message.expired / message.submitted_no_receipt webhook events. The dispatcher’s STATUS_TO_EVENT map (apps/api/src/routes/webhooks/dlr-common.ts) fans both out on message.failed, matching the per-status webhook table in the message status lifecycle reference. Subscribe to message.failed and branch on the payload’s status field to distinguish them. The MESSAGE_EXPIRED / MESSAGE_SUBMITTED_NO_RECEIPT constants in packages/shared/src/constants.ts are reserved for a future multi-event-per-status emitter and are not emitted today. expired — a delivery receipt arrived past the late-arrival window (DEVOTEL_DLR_LATE_ARRIVAL_MAX_AGE_DAYS) and the message can no longer be reconciled. state_class is terminal.
submitted_no_receipt — the provider accepted the submission (SMPP submit_sm ACK, or a Meta DM hand-off) but no delivery receipt returned within the grace window. state_class is intermediate — a later genuine delivered can still land, so do not treat this as a final outcome.

message.recipient_deactivated

Fired by the nightly carrier number-deactivation sweep once per deactivated MSISDN it flips to DNC (closing the TCPA-liability gap from texting recycled numbers). This is not tied to a single message, so it carries no message_id; instead it documents the deactivated number and its provenance. carrier may be null when the feed does not report it; source defaults to telnyx_feed.

Contact Events

contact.created

contact.updated

contact.deleted

contact.merged

contact.bulk_imported

Fired once per bulk import batch (CSV upload or POST /v1/contacts/bulk), not once per row. The payload summarises the whole batch.
This event is emitted whenever a batch changes at least one contact — that is, when any of imported, updated, or failed is nonzero. Update-only batches run in upsert mode, where every row matched an existing contact (imported: 0, failed: 0, updated > 0), do emit this event, so you can drive downstream sync directly from contact.bulk_imported. Only a no-op batch where all three counts are 0 does not emit.
Delivery semantics: at-least-once. This event rides the standard webhook delivery queue. On a transient receiver error (5xx, timeout) Orbit re-queues the delivery on an exponential backoff retry schedule — up to 9 retries with a 30-second base delay that doubles with each attempt (capping at ~4.3 hours total), each delay carrying +0–20% jitter (up to 20% longer than the nominal interval, never shorter), then moves the delivery to the dead-letter queue (DLQ). See the Retry Schedule for the full policy. Because of these retries the same contact.bulk_imported payload can be delivered more than once. Orbit does not guarantee exactly-once delivery on any webhook.Your consumer must be idempotent. Recommended dedupe key, in order of preference:
  1. Envelope id — the top-level event id is unique per delivery attempt’s logical event and is the canonical idempotency key for every Orbit webhook (id is stable across retries of the same event). Persist seen id values long enough to cover the full retry window — the active schedule is 10 attempts total on an exponential backoff starting at 30 seconds and doubling each retry (30s, 60s, 120s, …, plus jitter), spanning ~4.3h end-to-end before a delivery reaches the DLQ — plus the dead-letter replay window. A retention of at least 3 days comfortably covers both.
  2. data.import_batch_id — when non-null, import_batch_id is the natural key for the underlying batch. If you write summary counters into your own DB, key them on import_batch_id so a replay does not double-add the totals.
If neither key is available (import_batch_id: null and a malformed envelope), treat the delivery as a no-op observability signal and fetch authoritative state via GET /v1/contacts/import-jobs.

contact.opted_in / contact.opted_out

Fired when a recipient sends a START / STOP keyword on a registered channel.

contact.recertification_due

FCC RMD annual consent recertification window opened on a contact.

contact.phone_changed

Fired when WhatsApp (Meta) notifies Orbit that a customer moved to a new phone number. Orbit reconciles the matching contacts onto the new number and re-certifies their standing consent, then emits this event with the bulk outcome. This is a different contract from contact.recertification_due: it reports a completed re-certification across one or more contacts, not a single contact whose annual consent window just opened.
source_message_id is the originating WhatsApp message id and may be null. consent_recertified_count counts the consent records Orbit re-certified during the reconciliation and is always >= 1 for a delivered event.

contact.segment_changed

Predictive scorer flipped a contact’s segment label (e.g. champion → at_risk, passive → engaged). Useful for triggering re-engagement plays from a CRM.

Campaign Events

campaign.created / campaign.updated / campaign.deleted

campaign.started

campaign.completed

Fires once when a campaign reaches a terminal state. This is the terminal event for both success and failure paths — a campaign that never sends (its audience couldn’t be resolved, resolved to zero contacts, or had no addressable contacts) still emits campaign.completed, not a separate failure event. Branch on status, not on the presence of the event. status is one of:
  • completed — the campaign finished; every recipient was attempted.
  • partially_failed — the campaign finished, but some sends failed.
  • failed — the campaign ended without a successful send.
On failure and no-op paths the payload also carries completion_reason, one of:
  • audience_resolution_failed — the audience query errored before any send.
  • no_addressable_contacts — no contact had the address the channel needs (for example, an SMS campaign whose contacts have no phone number). status is failed.
  • no_recipients — the audience resolved to zero contacts. status is completed (nothing failed; there was simply nothing to send).
Count fields (sent_count, delivered_count, failed_count) are present when the campaign got far enough to attempt delivery. Success:
Terminal failure (audience could not be resolved):

campaign.paused / campaign.resumed

campaign.drip_step.completed

Single drip step finished sending to its slice.

campaign.drip.completed

Entire drip campaign finished its full schedule.

Voice Events

call.initiated

When the call is bridged through Jambonz, the payload also carries provider and direction context:

call.answered

call.completed

call.failed

call.transferred

data.transfer_type is one of blind, warm, or queue.

call.dtmf_sent

Mid-call DTMF inject through the API.

call.recording.paused / call.recording.resumed

SIPREC fork toggled mid-call.

call.secure_payment.started / call.secure_payment.completed / call.secure_payment.cancelled

Agent-assisted secure payment (masked-DTMF card capture, PCI Pal / Sycurio-class). Emitted by VoiceService.startSecurePaymentSession / finalizeSecurePaymentSession. While a session is started the SIPREC recording is paused and the caller’s keypad DTMF is masked from the agent (PCI-DSS agent descoping). The completed payload carries only the masked summary (card_last4, card_brand) — never the PAN. call.secure_payment.started:
call.secure_payment.completed — masked summary only:
call.secure_payment.cancelled:
After a session ends (completed or cancelled) the SIPREC recording resumes automatically — listen for call.recording.resumed.

call.recording.ready

call.transcription.ready

call.synthesis.ready (legacy inline pipeline)

call.synthesized (post-call synthesis pipeline)

Richer payload from the async synthesis scheduler. Gated on tenant opt-in.

call.ringing

Fires when an inbound call is detected by the SIP edge but before it is answered. Useful for “live caller is dialing in” UI updates without waiting for the post-answer call.answered event.
Trigger condition: carrier sends INVITE to the SIP edge. Retry behaviour: standard webhook retry (exponential backoff, 10 attempts over ~4.3 hours, with +0–20% jitter applied to delays — see Retry Schedule). Idempotent — call.ringing fires at most once per call. The emit is deduplicated by provider_call_sid for one hour, so a call routed across multiple ring-group legs still produces a single ringing callback. Note (current implementation): for inbound calls, call.initiated also fires with status: "ringing", so ringing is covered even if you only listen for call.initiated. The standalone call.ringing event uses the same subscription rules as every other event — list call.ringing in your endpoint’s events array, or use "events": ["*"]. If call.ringing is missing from your delivery, confirm your endpoint subscribes to it (or *), or fall back to listening for call.initiated with status: "ringing".

Conference Events

Conference lifecycle events fire from the conference service in apps/api/src/routes/voice/. The events ride the same delivery + retry semantics as call events.

conference.created

Fires when POST /api/v1/voice/conferences succeeds.

conference.participant_joined

Fires when a new participant successfully joins (after 200 OK to the SIP INVITE for that leg).

conference.participant_left

Fires when a participant disconnects — either via DELETE API call, BYE from the carrier, or the conference moderator’s end-conference action.
hangup_reason is the free-text disconnect cause reported by the carrier/softswitch (for example Normal Call Clearing, caller hangup, or RTP timeout) — it is not a fixed enum, so match on substrings rather than exact values. sip_response_code is the numeric SIP status for the leg (e.g. 200), or null when none was received.

conference.ended

Fires when the conference’s active participant count drops to zero, or moderator hits end-conference. Always the LAST event for a conference id.
end_reason values: all_legs_terminated (the active participant count dropped to zero), manual_end (a moderator or operator ended the conference), timeout (the max-duration TTL backstop closed the room). final_status carries the conference’s terminal status (completed, failed). ended_at is the UTC timestamp the conference closed.

Recording Events

recording.started

Fires when POST /api/v1/voice/calls/{id}/recording/start (or the per-call record:true flag) starts a fresh recording.
classification tells you which surface produced the recording so you can route the event without a follow-up API call. It is one of call, conference, ai_agent, browser_softphone, or video_room. Conference recordings carry conference_id in place of call_id.

recording.completed

Alias for call.recording.ready (same payload + same recording_url). Documented separately because the call-id may not be available for conference recordings — those carry conference_id instead.
classification tells you which surface produced the recording so you can route the event without a follow-up API call. It is one of call, conference, ai_agent, browser_softphone, or video_room.

recording.failed

Fires when an upstream recording attempt fails (storage write failure, codec mismatch, AMD aborted the recording before any audio was captured).
reason is the machine-readable failure cause — read it for the reason this recording failed. failed_at is when the failure occurred (it may predate timestamp, which is when the webhook was emitted). classification tells you which surface produced the recording (call, conference, ai_agent, browser_softphone, or video_room); conference recordings carry conference_id in place of call_id.

voicemail.received

voicemail.transcript_updated

Fires when an originally-missing voicemail transcript is recovered by the retry pipeline.

recording.retention_deleted

Fires 24h before a retention-driven GCS purge so tenants can mirror the asset to their own archive. Payload carries a signed download URL valid until purge_scheduled_at.

Agent Events

agent.created / agent.updated / agent.deleted / agent.deployed

agent.conversation.started

agent.conversation.ended

agent.handoff.requested

Agent escalated to a human operator.

agent.handoff_occurred

A conversation was handed to another specialist agent — either an autonomous transfer_to_agent delegation or an operator-initiated handoff run from an inbox macro. For macro-driven handoffs source_agent_id is operator_macro.

agent.calendar_event.created

Fires from the agent’s create_calendar_event MCP tool.

Flow Events

flow.created / flow.updated / flow.published / flow.deleted

flow.executed

Coarse terminal signal. status is completed, failed, or timeout.

flow.execution.started

flow.execution.completed

flow.execution.failed


Verification Events

The platform uses the verification.* prefix (not verify.*).

verification.sent

When a SIM-swap soft warning is detected, sim_swap_warning: true is included alongside the payload above.

verification.approved

For Silent Verification (SIM-card-network-binding flow), channel is "silent".

verification.failed

verification.failed is emitted only when verification fails because the code was entered incorrectly too many times (attempts exhausted). data carries only the fields above — there is no reason field. Do not branch on a reason value; treat any verification.failed event as the attempts-exhausted case. Pre-send security and fraud blocks are not reported through verification.failed. They are refused before any verification row exists (no verification_id, no OTP sent, no code checked), so reporting them as a failed verification would deliver a phantom failure for a verification that never started. Handle them as follows:
  • Fraud blocks are delivered as their own verification.fraud_blocked webhook (documented below).
  • SIM-swap blocks and line-type blocks emit no webhook at all — they surface only as synchronous API errors. Read the error response body on the send request (see below).

verification.fraud_blocked

Emitted when the pre-send Fraud Guard refuses an OTP send (the destination scored as fraudulent under the tenant’s fraud policy). This fires before any verification row is created or OTP is dispatched, so the payload deliberately has no verification_id.
The same send is also refused synchronously with a 403 VERIFY_FRAUD_BLOCKED API error whose details carry risk_score and reasons.

SIM-swap and line-type blocks (no webhook)

Pre-send SIM-swap and line-type policy blocks do not dispatch any webhook. They are returned synchronously as API errors on the verification send request, so subscribers must read the error response body rather than wait for an event:
  • SIM-swap block403 SIM_SWAP_DETECTED; details carry last_swap_date and block_window_hours.
  • Line-type block422 VERIFY_LINE_TYPE_BLOCKED; details carry line_type, channel, and policy_slot.

Number / Porting Events

number.purchased

reclaimed: true is included when the number was re-claimed from the parking pool.

number.released

number.ported

porting.request.cancelled

Customer aborted an in-flight port.

Conversation Events

conversation.created


List / Segment Events

list.updated fires for both metadata edits and membership changes. Every payload carries a change discriminator so you can tell why the list updated without diffing state: metadata (rename / description edit), member_added and member_removed (single membership change, each carries contact_id), or members_added (bulk add, carries added_count). The list_id and current name are always present. Membership events fire only when the list actually changed — an idempotent re-add or a no-op remove emits nothing.

Webhook Endpoint Events


Billing Events

subaccount.spend_cap.threshold

Fired to the parent (reseller) org when a capped subaccount’s month-to-date spend crosses 50%, 80%, or 100% of its monthly spend cap. Each threshold fires once per billing period and re-arms at the start of the next month. The 100 crossing carries enforced: true — the cap is now stopping outbound traffic for that subaccount. Subaccounts with no cap set are never evaluated. usage_percent is the floored month-to-date percentage of the cap (it can exceed 100 once spend runs past the ceiling).

balance.low

balance.topped_up

credits.purchased (deprecated alias of balance.topped_up)

Same data plus a legacy credits field. Prefer balance.topped_up for new integrations.

Channel Failover Events

channel.failover

Fired when a messaging channel’s primary provider failed and Orbit automatically switched to a backup.

Push Events

push.delivered

Mobile / web SDK acknowledged a push was rendered on-device.

push.opened

User tapped the push and the SDK reported the open.

Number Masking (Proxy) Events

proxy.session.created

proxy.session.closed

proxy.message.forwarded


WhatsApp Events

WhatsApp events mirror Meta WABA webhook fields. Each fires after Orbit ingests the corresponding Meta callback and persists the upstream change. Payloads carry the relevant waba_id / phone_number_id so consumers can correlate to their own Meta tooling.

WhatsApp Calling

The lifecycle events (whatsapp.call.connected, _accepted, _terminated) share the same payload shape; the legacy whatsapp.call.received is also dispatched on every transition for backward-compatible consumers.
whatsapp.call.permission_granted:
whatsapp.call.permission_revoked:

WhatsApp Templates

Sample for whatsapp.template.approved (others share the same shape with the relevant Meta payload echoed under details):
whatsapp.template.rejected includes details.reason with Meta’s rejection text.

WhatsApp Account / Business / Phone

whatsapp.account.update, whatsapp.account.banned, whatsapp.account.restricted, whatsapp.account.alert, whatsapp.account.review_update, whatsapp.account.settings_update, whatsapp.business.status_update, whatsapp.phone.quality_update, whatsapp.phone.name_update, whatsapp.capability.update, whatsapp.security.alert, whatsapp.flow.status_change, whatsapp.quality.changed — all share the shape:

WhatsApp Data Subject Requests

whatsapp.data.delete_request follows the same shape.

WhatsApp v25 fields

whatsapp.automatic_events, whatsapp.history, whatsapp.partner_solutions, whatsapp.payment_configuration_update, whatsapp.smb.app_state_sync, whatsapp.smb.message_echoes, whatsapp.tracking_events, whatsapp.user.preferences, whatsapp.group.lifecycle_update, whatsapp.group.participants_update, whatsapp.group.settings_update, whatsapp.group.status_update — each carries the original Meta event payload under details:

WhatsApp Order

whatsapp.order.received — conversational-commerce order capture. Fires once per inbound type === "order" message — i.e. when a buyer browses the merchant’s Meta Commerce catalog inside the WhatsApp thread and taps Send in the checkout drawer. Carries the normalized cart so merchants can drive fulfilment / capture payment in their own systems off a single dedicated event (previously the order only rode the generic message.received fan-out inside message metadata). De-duplicated against Meta webhook retries — it fires only on the first delivery of a given wamid.
Monetary fields (unit_price, line_total, subtotal) are minor units of currency. When a cart mixes currencies, mixed_currency is true and subtotal is omitted — read the per-item line_total values instead.

Survey Events

Standalone surveys product (surveys) response webhooks. Tenant-scoped — every emit flows through dispatchTenantWebhook, so subscribers only see responses for their own organization.

survey.response.detractor

CXaaS closed-loop survey feedback — fires when a recipient-facing survey response lands at detractor level (NPS ≤ 6 of 10, CSAT ≤ 6 of 10). Fires at most once per response, gated on the first recording of a responded_at (a repeat tap of the same signed link is idempotent and does not re-fire). Wire CRMs (Salesforce / HubSpot / Zendesk) to this event to open a case/activity within seconds of the detractor response.
survey_type is one of "nps", "csat", or "ces". score is the raw response value and threshold the detractor cutoff that was crossed; comment is null when the respondent left no free-text.

Calendly Events

Re-emitted when Calendly delivers a scheduling event to your connected Calendly integration. The four calendly.* types map one-to-one to Calendly’s own event names, with the raw Calendly type repeated in data.event_type: All four share one payload shape:
contact_id is the Orbit contact matched or created from the invitee email, or null when no match was made. invitee_email is null for routing-form submissions that carry no email. scheduled_event_uri points at the parent Calendly event; event_uri points at the specific invitee or submission record. The delivery is idempotent on Calendly’s event identity — a duplicate Calendly redelivery does not re-fire this webhook.

Video Events

Real-time video room lifecycle, recording, ingress, and transcription webhooks. Tenant-scoped — every emit flows through dispatchTenantWebhook. These mirror the Daily / Twilio Video / Agora callbacks so a dashboard can wake on a room start, a guest join, or a finished recording without polling the SFU.

video.room.started / video.room.ended

Fire when a video room opens and closes.
video.room.ended carries the same room_id, room_sid, and room_name, plus a full set of end-of-call fields:
end_reason is one of failed_no_connect (room opened but no participant ever connected), last_participant_left (emptied out normally), operator_end (closed from the dashboard or API), timeout (hit the configured max duration), room_finished (SFU closed the room), or error (closed after an internal failure). It mirrors the room’s persisted termination classification 1:1. room_sid is omitted when the SFU never assigned one.

video.participant.joined / video.participant.left

Fire when a participant joins or leaves a room.
video.participant.left carries { room_id, identity }.

video.recording.completed / video.recording.failed

Twilio-Video-parity completion callbacks for a room recording.
recording_id is the stable id for the recording, recording_url is the primary artifact, and recording_urls lists every artifact URL for the room (a single-file recording collapses to one entry that matches recording_url). recording_qc is only present once quality scoring has run. video.recording.failed carries { room_id, recording_url: null, status, reasons }.

video.recording.degraded

Recording QC verdict — fires alongside video.recording.completed when the artifact lands but the quality scorer flags it. Subscribe to this when you want strict missing / degraded semantics so silent recording loss is caught within minutes of call-end.
status is "missing" or "degraded"; recording_url is null when no artifact was produced.

video.active_speakers.changed

Fires whenever the SFU’s active-speaker set changes. The speakers array is pre-sorted descending by audio level, so speakers[0] is the dominant speaker. An empty array signals silence. Not persisted per event — aggregate downstream for talk-time rollups.

video.ingress.started / video.ingress.ended / video.ingress.failed

RTMP / WHIP / URL-pull ingress lifecycle. Subscribe to video.ingress.started to wake live on a broadcast source connecting, and to the ended / failed pair for terminal states.
video.ingress.ended adds status ("complete" or "error"), error, and duration_seconds. video.ingress.failed carries a required error string. A source that errors fires both video.ingress.failed and video.ingress.ended with status: "error".

video.recording.transcript_ready

Async post-meeting transcription of a room recording is complete (Deepgram). Zoom / Daily.co transcript-ready parity.

video.caption.segment

Fired per completed (final) live caption line during an active room, so a downstream system can consume captions in real time — a live translation relay, a compliance-capture pipeline, an accessibility feed, or live-blogging — instead of waiting for the post-recording video.recording.transcript_ready event. Interim/partial results are never emitted. room_id is the same room id every other video event carries, so you can correlate segments with the room’s lifecycle events.

Participant moderation events

Fired when a host or moderator acts on a participant, or when a participant’s in-room state changes — so you can drive a moderation UI, a compliance-logging pipeline, or webinar tooling programmatically (parity with Twilio Video / LiveKit / Agora participant-event streams). The events are: Every payload carries room_id (the same room id every other video event carries, so you can correlate against video.room.started / video.participant.joined), the server-stamped actor_user_id and actor_role of the moderator who performed the action, and emitted_at. Per-event fields: identity (the affected participant — absent on the room-level video.room.spotlight_cleared), kind (audio / video, on mute / unmute), reason (on ban), role (on role change), and hand_id (on hand raised).

Supervisor Events

Live-call supervisor takeover (warm-handoff) lifecycle. Tenant-scoped.

supervisor.takeover_requested

A supervisor initiates a takeover of a live call.

supervisor.takeover_completed

The supervisor successfully takes over and becomes the active party.

supervisor.takeover_failed

The attempt failed (e.g. a REST error during the leg-swap).

supervisor.takeover_cancelled

The takeover was cancelled (call ended before handoff completed, or the supervisor aborted).

Voice Queue (ACD) Events

Contact-center queue-routing lifecycle (Five9 / Genesys parity). Wire these for screen-pop, after-call work, and abandonment-rescue flows without polling the queue-entries endpoint. Tenant-scoped.

queue.call.queued

Fires the instant a call enters a queue.

queue.call.assigned

Fires the moment an agent is selected, before the origination call returns — drives screen-pop.

queue.call.abandoned

Fires when the caller hangs up before assignment, or the ACD reconciler sweeps a stuck row.

queue.alert_rule_fired

A user-configured supervisor-wallboard alert rule (action webhook) crossed its threshold. Operators may override the emitted event slug per rule via the rule’s delivery_config.webhook_event_type.

agent.state.changed

Contact-center agent presence transition (WFM / RTA integration parity). Distinct from the AI-agent lifecycle events — this carries the CCaaS from/to presence pair the supervisor wallboard sees.

wallboard.sla_warning

Fires when a queue’s predicted total wait crosses 90% of its maxWaitSeconds SLA target. One event per breach window (30s per-queue dedup). Wire to PagerDuty / Opsgenie / on-call Slack.

wfm.adherence_breach

Fires when an agent’s intraday adherence ratio drops below the out-of-adherence threshold (default 85%). At most once per ~5 minutes per agent. NICE IEX / Verint / Genesys WFM parity.

Post-Call IVR Survey Events

IVR-captured post-call CSAT / NPS responses (distinct from the standalone Surveys product above). Fires once per captured response — the de-dup path does not re-fire. Wire CRMs to auto-create activities / cases on a low CSAT or detractor NPS within seconds of the call ending. Tenant-scoped.

survey.csat.response_recorded / survey.nps.response_recorded

Siblings carry the same shape — discriminate by type. CSAT uses a 1-5 scale, NPS uses 0-10.
CSAT responses carry satisfaction_bucket (satisfied / neutral / dissatisfied); NPS responses instead carry nps_bucket (promoter / passive / detractor). Idempotent on the (survey_id, call_id) tuple, so a jambonz retry does not double-fire.

Conversation & Inbox Events

AI- and rule-driven conversation lifecycle for the inbox / CXaaS surface. Tenant-scoped.

conversation.closed

A conversation transitioned to a closed/resolved terminal state.

conversation.disposed

The auto-disposition engine automatically closed/resolved a conversation.

conversation.scored

A conversation received a quality-management (QM) score.

conversation.csat_detractor

A customer-facing CSAT (1-5, score under 3) or NPS (0-10, score 6 or below) submission lands and qualifies as a detractor — a “go-recover-this-customer” alert distinct from conversation.scored. Gated on the first write, so a resubmission does not double-fire.
auto_reopened is true when the tenant opted into settings.csat.auto_reopen_on_detractor and the reopen succeeded.

conversation.synthesized

A structured post-chat AI summary landed on the conversation (Intercom Fin / Sierra parity). Mirrors the voice-surface call.synthesized shape so a CRM/CDP can attach the summary without re-summarising.

conversation.deflected / ticket.deflected

An AI knowledge-base auto-reply resolved a conversation without a human. Subscribe to both conversation.closed and conversation.deflected to separate AI-resolved from human-resolved volume (AI-containment-rate / deflection KPIs). ticket.deflected is the symmetrical alias for CRMs that model conversations as tickets — same payload, same code path.

conversation.ai_review_needed

An AI-resolved conversation received a low CSAT — pairs every deflection with a satisfaction check and auto-flags “AI deflected but the customer was unhappy” for human review and retraining.

CDP Events

Customer-data-platform ingestion, identity resolution, computed-trait, and segment-membership webhooks (Segment / RudderStack / Hightouch parity). Tenant-scoped.

cdp.event.ingested

Fires after a server-to-server /cdp/track or /cdp/identify payload is verified, persisted, and identity-resolved.

cdp.identity.resolved

Fires after an identify call resolves an anonymous identity onto a contact — for downstream CDP-to-CDP federation.
created is true when the identify call created a new contact.

cdp.tracking_plan.violation_spike

Proactive data-quality alert — fires when a tenant’s tracking-plan / event-schema violation volume crosses the alert threshold in the rolling window (Segment Protocols / RudderStack Data Governance parity).

contact.trait_changed

A computed-trait value transition for a single contact (Hightouch / Segment computed-traits parity) — fires when the realtime recompute pipeline observes a change to a contact’s trait. Critical for journey triggers (e.g. a contact crossing ltv >= 1000).

contact.entered_segment / contact.exited_segment

Fires once per contact who crosses an operator-defined segment’s membership boundary on its auto_refresh tick (distinct from the predictive contact.segment_changed). Wire to Braze / Customer.io / Iterable to mirror audience membership in real time.
contact.exited_segment carries the same shape with direction: "exited".

Integration Health Events

Nango-managed CRM / helpdesk / automation connector health. Surfaces a previously-silent failure class so you learn the moment a tenant’s data stops flowing rather than on the next manual “Test connection”. Grouped under the integration prefix in the subscribe UI. Tenant-scoped.

integration.auth_failed

A stored OAuth refresh token was rejected (revoked, scope changed, account de-provisioned) — the connection silently rots while the UI still shows “Connected”.

integration.sync_failed

A sync errored (token expired mid-sync, upstream 5xx, flow-script failure). Fired from two paths, distinguished by reason. reason is a stable platform-side classifier, never raw upstream provider error text. The payload shape depends on which path fired, so branch on reason rather than assuming a field is present. When a specific sync run fails, the event carries the full connection context and reason is sync_failed:
When a connection’s consecutive-failure count crosses the alerting threshold, only the provider is included and reason is consecutive_failure_threshold:

Account & Security Events

account.fraud.alert

A customer-subscribable real-time signal that the platform’s fraud / abuse detection acted on your own traffic — toll-fraud / IRSF prefix block, SMS traffic-pumping, SIM-swap heuristic, voice-deepfake high score, or an API-key geo anomaly. The customer-facing fan-out of the same detection decision that lands in the internal fraud queue, so you learn within minutes instead of from the next invoice. All values are PII-scrubbed (phone PREFIX only). Tenant-scoped.
category is one of irsf_blocked, sim_swap_detected, suspicious_campaign_send, voice_deepfake_high_score, or api_key_geo_anomaly. severity is low / medium / high / critical.

audit.log.created

Continuous audit-log streaming to a customer SIEM — fires once per append to the audit log, giving SOC teams near-real-time push of every security event to their own sink. HMAC-signed with the same envelope as every other Orbit webhook. Tenant-scoped.
current_hash is the tamper-evident hash-chain digest of the row, so a SIEM can later cross-check the streamed event against the audit-export verify endpoint.

Slack Integration Events

Re-emitted from the tenant Slack webhook receiver after the inbound Slack payload is idempotently persisted, so a tenant integration can react outside Slack’s 3-second ack budget. Grouped under the slack prefix in the subscribe UI. Tenant-scoped.

slack.slash_command

A Slack slash command (e.g. /orbit send-sms) was received.

slack.event_received

A Slack Events API callback (app_mention, message, reaction_added, …) was received. The concrete Slack event type is carried in data.slack_event_type rather than folded into the wire name.

slack.interactivity

A Slack interaction (block_actions, view_submission, shortcut, …) was received. The interaction type is carried in data.interactivity_type.

Recording Lifecycle Events

Post-call recording QC, chaptering, and compliance-redaction webhooks (unified across SBC, AI-agent, browser-softphone, conference, and video recordings). Tenant-scoped.

recording.qc_failed

A post-finalize QC failure — distinct from recording.failed, which signals an egress abort BEFORE an artifact lands. This fires AFTER the artifact exists but the scorer found degradation (zero-byte file, missing path, late finalize, mime-type mismatch, sub-minimum duration).

recording.chapters.ready

The post-call auto-chaptering pipeline persisted a navigable chapter array onto a recording.
Carries either call_id or video_room_session_id as the surface key.

recording.redaction_vault.ready

The post-call redaction-vault pipeline produced a compliance-safe redacted copy of the call transcript. Gated on the per-tenant settings.compliance.recording_redaction_vault.enabled opt-in. For regulated (HIPAA / PCI-DSS / GDPR) tenants.
total_redactions is the count of regulated spans replaced across all categories. by_category lists per-category match counts (only categories with at least one match); type uses the redaction-engine category vocab (credit_card, ssn, email, e164_phone, us_phone, ipv4, ipv6). audio_redaction_segments carries the millisecond audio ranges to mute or beep on playback — each entry is { start_ms, end_ms, category } and only carries timings plus category, never raw transcript values. The list is empty when word-level timings are unavailable. timestamp is the ISO-8601 time the redacted copy was produced.

Voice Analytics Events

call.hit_voicemail

An outbound call hit a voicemail / fax (answering-machine detection result). Lets campaigns retry, log, or branch without waiting for the full hangup event.
amd_type carries the answering-machine-detection result — one of machine, machine_beep_detected, fax, ios_call_screening, or ios_live_voicemail. detected_at is the ISO-8601 time detection landed. direction is outbound or inbound, and from / to are the E.164 call legs (either may be null when the number is withheld). agent_id is present only when the call is tied to an AI voice agent.

call.transcript_ready

Fires after post-call sentiment + keyword + topic analysis lands on the call log — a first-class telephony CDP signal the segment-builder can target. Fans to both webhook endpoints and CDP subscriptions.
sentiment is positive / negative / neutral / mixed; sentiment_score ranges -1.0 to +1.0.

Verification Lifecycle Events

Per-attempt and async-fallback verification webhooks. Tenant-scoped.

verification.checked

Fires on EVERY persisted check attempt — both terminal outcomes (approved / failed) and the non-terminal pending (wrong code, attempts remaining). Emitted before the conditional verification.approved / verification.failed dispatch, so subscribers see a consistent per-attempt stream. Twilio Verify verification_check.completed parity.
outcome is approved / pending / failed.

verification.fallback_triggered

The async fallback engine successfully advanced to a new channel. Dedup-guarded so a scheduler retry does not double-fire.
reason is timeout or delivery_failed.

verification.fallback_exhausted

The fallback engine tried every configured channel without obtaining a verified status. The verification row is set to failed before this fires. phone is masked (first five characters, then ****) and exhausted_at is the ISO 8601 timestamp at which the engine gave up.

Number Lifecycle & Port-Out Events

DID inventory, compliance, and port-out lifecycle webhooks. Tenant-scoped.

number.updated

Fires on every successful per-DID mutation. Suppressed for a no-op edit (an empty PUT that changes no caller-supplied field).

number.released_compliance_timeout

A number was auto-released because its compliance deadline elapsed without the carrier activating it. The captured monthly cost is refunded to the tenant wallet. Distinct from the operator-initiated number.released so you can branch on a number lost to a missed deadline.

number.inventory.low

A low-DID-inventory alert per (country_code, area_code_prefix) bucket whose available count fell below the threshold. Wire to a dashboard banner or auto-reprovision via the Numbers API before run-out.

porting.request.loa_signed

A port-IN Letter of Authorization was signed in-platform — “ready to submit to carrier”. Carries the signer details so audit dashboards reconcile without an extra fetch.

number.port_out.requested / number.port_out.cancelled

Outbound port lifecycle — a DID Orbit hosts is being moved to (or kept from leaving for) another carrier. Lets you reconcile downstream billing and contact lists when a DID leaves the platform.
number.port_out.cancelled carries the same identifiers for an aborted port-out.

SIP Trunk Events

sip_trunk.status_changed

A customer SIP trunk’s registration state transitioned between registered and unregistered. Emitted by the register-poll reconciler only on a genuine edge — a still-down trunk re-probed unregistered does NOT fire, so a flapping registrar yields one event per real transition, not one per poll tick. Tenant-scoped.

AI Agent Events

agent.response

Fires after an AI agent finalizes an LLM turn (both the synchronous request/response API and the SSE-stream API). Tenants subscribed to agent.response receive the same structured envelope the webhook tester documents. Best-effort — a delivery failure never blocks the API response. Tenant-scoped.
tokens_used is the full billed turn, not just the visible reply. It counts the prompt context the model actually reads — your system prompt, tool definitions, retrieved knowledge-base passages, memory, and earlier turns in the conversation — plus the generated response. That context dominates the total, so tokens_used is normally far larger than the reply text suggests, and can exceed a naive character count of the response (tokenization is model-specific; non-Latin scripts cost roughly 2–3 tokens per character). It always reconciles as tokens_used === prompt_tokens + completion_tokens — in the example above, 1742 + 16 = 1758. Use the prompt_tokens / completion_tokens split to attribute spend between context and generation.

agent.dnc.marked

Fires when an AI voice / text agent invokes its mark_dnc tool and the platform persists the suppression. Wire into your CRM / consent-of-record system.

Campaign Drip Events

campaign.drip_step.sent

Fires once per (contact, step) the moment a drip-step message is enqueued successfully. Companion to campaign.drip_step.completed, which fires once per step as the aggregate. Tenant-scoped.

Billing Lifecycle Events

Stripe-driven billing webhooks fanned out to tenant subscribers. The Stripe envelope is deduplicated, so subscribers receive exactly one delivery per source event. Tenant-scoped.

invoice.created

A draft invoice was opened (cycle boundary, metered-usage threshold, or manual invoice).
period_start, period_end, and due_date are Unix timestamps (or null).

payment.failed

A PaymentIntent failed (e.g. an auto-topup card decline). Distinct from an invoice payment failure — PI-driven failures originate from the recharge path and carry no invoice id. Wire into dunning / customer- service tooling.

subscription.updated

A billing-subscription state change (e.g. scheduled cancellation, reactivation, plan change). Carries the action that drove the change.

credits.low

Deprecated alias of balance.low. Prefer balance.low for new integrations.

WhatsApp Template & Call Lifecycle Events

whatsapp.template.quality_update

Meta reported a template quality-rating change. The Meta webhook value fields are spread into data alongside waba_id.

whatsapp.template.category_update

Meta auto-recategorized a template between MARKETING / UTILITY / AUTHENTICATION. Orbit also re-syncs the local templates.category so cost tracking prices off Meta’s current category.

whatsapp.template.components_update

Meta auto-edited a template’s components for compliance (e.g. inserting the standard opt-out hint into a MARKETING body). Orbit re-syncs the local templates.content to Meta’s truth.

whatsapp.call.accepted / whatsapp.call.terminated

WhatsApp Business Calling lifecycle. Fan out alongside the legacy whatsapp.call.received event. Both carry the same payload shape; phase discriminates the lifecycle stage.
whatsapp.call.terminated carries the same shape with phase: "terminated" and a populated duration_seconds.

Voice Eval Events

Voice-agent regression-suite lifecycle webhooks. Tenant-scoped.

voice_eval.run_started / voice_eval.run_completed

Fire when an eval run begins and finishes (pass or fail).
voice_eval.run_started carries { run_id, agent_id }.

voice_eval.regression_detected

Fires when P95 latency or judge score regresses beyond the threshold vs the baseline run.

Subscribing to Events

Register a webhook and specify which events to receive:
Use "events": ["*"] to subscribe to all event types. Subscribing to a name not listed on this page returns a 422.

See also