Open in
Events Catalog
All webhook event types ThunderPhone emits.
Every webhook body has a type field whose value is one of the event
types on this page. When you subscribe to an
endpoint, the events array must contain the
event types you want (or be empty to subscribe to everything —
except the per-turn events telephony.turn /
web.turn, which are delivered only to endpoints that
name them explicitly).
Two delivery styles carry these events:
- Endpoint deliveries are always non-blocking notifications
with retries: respond with
any 2xx; the envelope carries an
event_idto dedup on. - Blocking exchanges run only on the
legacy single-URL webhook: the
telephony.incoming/web.incomingconfiguration request (webhook-mode numbers: 13 s including one retry; webhook-mode widget keys: 10 s) and webhook-mode tool dispatch. Your response shapes the live call.
Example payloads below show the endpoint envelope in its wire order
(keys sorted alphabetically: data, event_id, type); legacy
deliveries carry the same data without event_id.
Call events
telephony.incoming
Sent when an inbound call reaches one of your
phone numbers. Endpoint deliveries are
fire-and-forget notifications sent for every inbound call, whether
the number is agent-configured or webhook-configured. Numbers without
an assigned agent additionally receive the blocking configuration
request on the legacy webhook — see
telephony.incoming / web.incoming for
the full request / response schema.
{
"data": {
"call_id": 987654321,
"from_number": "+14155550199",
"to_number": "+15551234567"
},
"event_id": "3f6b2ad0-1c9e-4a57-9f2b-8f6f0f9d2f11",
"type": "telephony.incoming"
}| Field | Type | Description |
|---|---|---|
agent_id | integer | null | Agent that handled the call, when one was assigned |
agent_name | string | null | Agent that handled the call, when one was assigned |
telephony.complete
Sent when an inbound or outbound telephony call ends. Non-blocking.
Includes the transcript, recording URL when available, and billing summary. See
telephony.complete / web.complete for
the payload schema.
| Field | Type | Description |
|---|---|---|
agent_id | integer | null | Agent that handled the call, when one was assigned |
agent_name | string | null | Agent that handled the call, when one was assigned |
telephony.tool
Sent after a telephony call invokes a
function tool. Non-blocking audit notification —
the tool has already executed when this event is delivered; it covers
your own function tools and the built-in send_sms tool (not other built-in,
knowledge-base, app-connection, or MCP tools). For send_sms,
both arguments.to and arguments.body are [REDACTED]. Completion
transcripts also redact them; retrieve the content and recipient from the
organization-scoped SMS resource using the response's message_id.
{
"data": {
"arguments": { "date": "2026-04-21" },
"call_id": 987654321,
"from_number": "+14155550199",
"response": {
"response": { "available_slots": ["9:00 AM", "2:00 PM"] },
"status": 200
},
"to_number": "+15551234567",
"tool_name": "search_appointments"
},
"event_id": "1f0a7c3e-52d4-4a0e-8f4b-b1a6a1c0d9e2",
"type": "telephony.tool"
}response is the executed result: {"status": <http status>, "response": <your endpoint's JSON>} on success, or
{"status": <status>, "error": "<message>"} on failure.
| Field | Type | Description |
|---|---|---|
agent_id | integer | null | Agent that handled the call, when one was assigned |
agent_name | string | null | Agent that handled the call, when one was assigned |
telephony.turn
Sent while a telephony call is in progress, once for each
speech-bearing turn as it happens — the agent's spoken completions and
the caller's transcribed turns. Lets you follow the live conversation
over plain webhooks instead of polling
GET /v1/calls/{call_id}/transcript.
Non-blocking.
{
"data": {
"call_id": 987654321,
"entry_type": "completion",
"from_number": "+14155550199",
"position": 7,
"role": "assistant",
"start_ms": 15200,
"text": "How many employees does your company have?",
"to_number": "+15551234567"
},
"event_id": "8d3f5a2c-7b1e-4c9a-b6d0-2e4f6a8c0d1e",
"type": "telephony.turn"
}| Field | Type | Description |
|---|---|---|
agent_id | integer | null | Agent that handled the call, when one was assigned |
agent_name | string | null | Agent that handled the call, when one was assigned |
position | integer | The turn's index in the call history — a stable identity for ordering |
role | string | assistant (agent speech) or user (caller speech) |
text | string | The turn's transcript text as known at emission time |
entry_type | string | The underlying history entry type: completion (agent), or user_turn / span (caller) |
start_ms, end_ms | integer | Audio offsets in ms since call start; present only when playout timing was already known at emission time |
web.incoming
The web-channel equivalent of telephony.incoming, sent when a
web widget session or a builder mic test call
starts. Endpoint deliveries are fire-and-forget for every web session.
Publishable keys in mode="webhook" additionally receive the
blocking configuration request on the legacy webhook — that
blocking request has a different shape (origin_domain,
publishable_key_prefix; no phone numbers). See
telephony.incoming / web.incoming.
{
"data": {
"call_id": 987654322,
"from_number": "web",
"origin_domain": "https://example.com",
"publishable_key_prefix": "pk_live_a1b2",
"to_number": "+15551234567"
},
"event_id": "9a2b4c6d-8e0f-4a1b-9c2d-3e4f5a6b7c8d",
"type": "web.incoming"
}from_number is always the literal "web". For webhook-mode widget
sessions to_number is empty (the session's agent number is assigned
after configuration); for builder mic test calls origin_domain and
publishable_key_prefix are empty.
| Field | Type | Description |
|---|---|---|
agent_id | integer | null | Agent that handled the call, when one was assigned |
agent_name | string | null | Agent that handled the call, when one was assigned |
web.complete
The web-channel equivalent of telephony.complete, covering web
widget calls (direction: "web") and builder mic test calls
(direction: "test"). Non-blocking. Same payload shape as
telephony.complete, plus origin_domain,
with from_number set to "web".
| Field | Type | Description |
|---|---|---|
agent_id | integer | null | Agent that handled the call, when one was assigned |
agent_name | string | null | Agent that handled the call, when one was assigned |
web.tool
The web-channel equivalent of telephony.tool. The data carries
origin_domain instead of from_number / to_number.
| Field | Type | Description |
|---|---|---|
agent_id | integer | null | Agent that handled the call, when one was assigned |
agent_name | string | null | Agent that handled the call, when one was assigned |
web.turn
The web-channel equivalent of telephony.turn,
covering web widget calls and builder mic test calls. Same payload
shape, with origin_domain instead of from_number / to_number.
Like telephony.turn, it requires an explicit subscription — it
is never delivered through an empty events array.
| Field | Type | Description |
|---|---|---|
agent_id | integer | null | Agent that handled the call, when one was assigned |
agent_name | string | null | Agent that handled the call, when one was assigned |
call.transfer_completed
Sent once per call with the final outcome of a
call transfer: a cold transfer, or the human
bridge a warm transfer leaves behind after the agent drops out.
Non-blocking, and delivered to endpoints only (never to the legacy
single-URL webhook). It arrives after the call's telephony.complete:
once the carrier takes the call or the transfer fails, or, when
ThunderPhone bridged the call, once the bridge ends.
{
"data": {
"agent_id": 12,
"agent_name": "Front desk",
"bridge_kind": "cold_fallback",
"bridge_seconds": 255,
"bridge_total_cents": 5,
"call_id": 987654321,
"destination": "+15555550102",
"sip_status": 403,
"transfer_outcome": "bridged"
},
"event_id": "5b0f6c1e-9d2a-5e7b-8c4d-1a2b3c4d5e6f",
"type": "call.transfer_completed"
}| Field | Type | Description |
|---|---|---|
call_id | integer | The call that was transferred |
agent_id | integer | null | Agent that handled the call, when one was assigned |
agent_name | string | null | Agent that handled the call, when one was assigned |
transfer_outcome | string | refer_accepted: the carrier took the call over. bridged: ThunderPhone kept the caller and the destination connected itself, either because the carrier refused a cold transfer or because a warm transfer's agent dropped out. bridge_unanswered: the carrier refused a cold transfer and the destination did not answer ThunderPhone's own call. failed: the transfer did not go through and the call ended |
destination | string | null | The number the call was transferred to |
sip_status | integer | null | The carrier's SIP status when it refused the cold transfer: 403 (for example a Twilio trunk without call transfer), 405 or 501. null when the carrier did not refuse it |
bridge_kind | string | null | bridged only: cold_fallback (a refused cold transfer ThunderPhone connected) or warm (a warm transfer's two people after the agent left) |
bridge_seconds | integer | null | bridged only: the bridged time ThunderPhone billed, in seconds |
bridge_total_cents | integer | null | bridged only: what the bridge cost in USD cents after any free call credit, billed separately from the call's billing_total_cents |
The same values are on the call object
as transfer_outcome, transfer_destination, transfer_sip_status,
transfer_bridge_kind, transfer_bridge_seconds and
transfer_bridge_total_cents. event_id is the same on every delivery
for a call, so a repeated report never produces a second event.
SMS events
SMS events describe messages sent through the organization's connected
Twilio or Telnyx account. The message body is omitted and to is redacted.
Retrieve the organization-scoped message from
GET /v1/sms/{message_id} when its
content or recipient is needed.
sms.sent
Sent when the customer's carrier accepts an outbound SMS request. This is not a handset delivery receipt.
{
"data": {
"accepted_at": "2026-09-19T15:42:10.113Z",
"agent_id": 12,
"call_id": 987654321,
"created_at": "2026-09-19T15:42:09.901Z",
"direction": "outbound",
"error_code": null,
"error_message": null,
"from_number": "+15551234567",
"id": "313a3117-827b-46e9-a01a-ac1493838724",
"provider": "twilio",
"provider_id": "SM0123456789abcdef0123456789abcdef",
"segment_count": 1,
"source": "agent_tool",
"status": "accepted",
"to": "[REDACTED]"
},
"event_id": "0ae347b4-1716-45bb-9a0e-b52373586e37",
"type": "sms.sent"
}sms.failed
Sent after a permanent carrier rejection, after transient retries are
exhausted, or when the outcome is terminally unknown. error_code is a stable
ThunderPhone code and error_message states the customer action when one is
available. status: "unknown" means the carrier may have accepted the SMS;
reconcile it in the carrier log because ThunderPhone will not resend it
automatically.
sms.received
Reserved for inbound SMS support. No sms.received events are emitted in the
current release. Inbound carrier webhook configuration and signature
verification will ship separately.
Voice events
Custom voice creation is asynchronous. These non-blocking events let you react to a terminal result instead of polling the clone detail endpoint.
voice.ready and voice.failed are delivered only to an organization-wide
endpoint with events: []. They cannot be selected as explicit event filters.
voice.ready
Sent when a custom voice finishes processing and can be assigned to an agent.
{
"data": {
"voice": {
"created_at": "2026-07-30T14:12:08.317Z",
"display_name": "Support voice",
"failure_reason": "",
"gender": "female",
"id": "cv_2f6f90b0e9a34ee8b39be7d1",
"language": "en",
"name": "custom:cv_2f6f90b0e9a34ee8b39be7d1",
"status": "ready",
"updated_at": "2026-07-30T14:13:31.605Z"
}
},
"event_id": "2d5f0a61-e9b5-4a3c-b684-29d7d9e4b214",
"type": "voice.ready"
}voice.failed
Sent when custom voice processing reaches a permanent failure.
{
"data": {
"reason": "audio sample could not be processed",
"voice": {
"created_at": "2026-07-30T14:12:08.317Z",
"display_name": "Support voice",
"failure_reason": "audio sample could not be processed",
"gender": "female",
"id": "cv_2f6f90b0e9a34ee8b39be7d1",
"language": "en",
"name": "custom:cv_2f6f90b0e9a34ee8b39be7d1",
"status": "failed",
"updated_at": "2026-07-30T14:13:31.605Z"
}
},
"event_id": "3493e985-1a75-4f77-a10a-e74af440cd31",
"type": "voice.failed"
}| Field | Type | Description |
|---|---|---|
voice.id | string | Custom voice public id |
voice.name | string | Agent voice value in the form custom:<public_id> |
voice.display_name | string | Organization-facing voice name |
voice.language | string | The clone's single language code |
voice.gender | string | male, female, or an empty string |
voice.status | string | ready for voice.ready; failed for voice.failed |
voice.failure_reason | string | Empty on success; processing failure detail on failure |
voice.created_at, voice.updated_at | timestamp | ISO 8601 timestamps |
reason | string | Failure detail; present only on voice.failed |
Quality events
call.graded
Sent whenever an AI grading run completes for a call. Non-blocking.
{
"data": {
"call_id": 987654321,
"grade": {
"call_outcome": "success",
"created_at": "2026-04-20T18:25:11.002Z",
"detected_issues": [],
"graded_at": "2026-04-20T18:25:11.002Z",
"grader_model": "heuristic-v1",
"id": 5512,
"score": 92,
"status": "completed",
"summary": "Caller asked about their policy and got a full answer…"
},
"report": null
},
"event_id": "7c1d2e3f-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
"type": "call.graded"
}| Field | Type | Description |
|---|---|---|
agent_id | integer | null | Agent that handled the call, when one was assigned |
agent_name | string | null | Agent that handled the call, when one was assigned |
grade.id | integer | Grade id |
grade.score | integer | null | 0–100 |
grade.call_outcome | string | success, failure, unknown, or no_conversation |
grade.summary | string | One-paragraph summary |
grade.detected_issues | array | Issue strings found by the grader |
grade.status | string | Always completed — only finished runs emit |
grade.grader_model | string | Which grader produced the result (e.g. heuristic-v1) |
grade.graded_at, grade.created_at | timestamp | |
report | object | null | The customer-facing call report: outcome label, needs-attention flag and reason, caller, one-line summary, caller experience. On an early heuristic grade it is null or a pending report whose fields are placeholders; the full grade's event carries the completed one |
call.data_extracted
Sent whenever structured data extraction completes successfully, including a
late retry after telephony.complete / web.complete or a manual re-run via
POST /v1/calls/{call_id}/extract.
Non-blocking.
In a blocking extraction mode, the completion event normally waits no more
than the 75-second extraction budget. If the extraction worker process is
lost, the production finalization drain (every five minutes) releases a
completion whose blocking_deadline_at has passed before it starts another
extraction attempt. A later success is delivered separately as this event.
{
"data": {
"call_id": 987654321,
"agent_id": 12,
"agent_name": "Acme intake",
"extracted_data": {
"status": "completed",
"fields": {
"customer_name": "Alex Morgan",
"appointment_date": "2026-04-23"
},
"evidence": {
"customer_name": {
"quote": "My name is Alex Morgan",
"speaker_role": "caller",
"turn_index": 4
},
"appointment_date": {
"quote": "April 23 works for me",
"speaker_role": "caller",
"turn_index": 7
}
},
"verification": "verified",
"field_reasons": {},
"schema_version": "92850758e231a3c95a..."
},
"extracted_at": "2026-04-20T18:25:11.002Z",
"model": "<model id>",
"summary": "Alex Morgan called to book a follow-up visit and confirmed Thursday, April 23."
},
"event_id": "4d79ef1d-c2b1-4ed6-85b8-8326bd2895ef",
"type": "call.data_extracted"
}| Field | Type | Description |
|---|---|---|
agent_id | integer | null | Agent that handled the call, when one was assigned |
agent_name | string | null | Agent that handled the call, when one was assigned |
summary | string | null | The completed call summary when the agent has Generate a call summary on, so a late extraction event also delivers a summary that missed the completion window; null otherwise. Agents with a summary but no extraction fields never emit this event: read a late summary from GET /v1/calls/{call_id} |
extracted_data.status | string | Always completed for this event |
extracted_data.fields | object | Values keyed by the configured extraction field keys; unavailable values are null |
extracted_data.evidence | object | Evidence keyed by extraction field. A non-null value has the exact structurally checked quote (at most 1,000 characters), speaker_role (caller or agent), and turn_index; longer model-returned quotes are rejected instead of truncated, and evidence is null whenever its field is null |
extracted_data.verification | string | verified only when the independent evidence pass returned exactly one valid verdict for every candidate field. unavailable means the pass failed, timed out, lacked enough budget, or returned malformed or partial output. A wholly unavailable pass preserves structurally grounded values for customer review. For partial output, valid verdicts are applied and each candidate without exactly one valid verdict is nulled |
extracted_data.field_reasons | object | Reasons keyed by fields that were nulled by structural grounding or the independent verifier |
extracted_data.schema_version | string | Hash of the exact field schema used for this extraction |
extracted_at | timestamp | ISO 8601 completion time |
model | string | Model used for the extraction |
campaign.completed
Sent once when a campaign moves from running to completed, whether its
schedule ended or all contacts reached terminal states. Runner retries do not
emit another event. This organization-level lifecycle event is delivered only
to organization-scoped endpoints, not agent-scoped endpoints. Non-blocking.
{
"data": {
"campaign_id": "3f6b2c9e-2a0d-4c63-b6d6-a708dc98f403",
"name": "May win-back",
"agent_id": 12,
"status": "completed",
"started_at": "2026-04-20T17:00:00Z",
"completed_at": "2026-04-20T18:25:11Z",
"counts": {
"contacts_total": 150,
"completed": 121,
"failed": 11,
"no_answer": 18,
"remaining": 0
}
},
"event_id": "8d8f52ce-6b46-423f-9dde-cea0b91ec135",
"type": "campaign.completed"
}The four outcome counts are disjoint and add up to contacts_total:
completed contains successful contacts; no_answer contains terminal
failed/exhausted contacts whose final outcome was no answer; failed contains
all other terminal failed/exhausted contacts; and remaining contains pending,
scheduled, or currently calling contacts. A contact waiting for a retry is
remaining, even when its most recent attempt was no answer. In-flight calls
are reconciled before the once-only completion snapshot. started_at is the
configured campaign start, or the campaign creation time when no start was
configured.
issue.reported
Sent when an issue report is created —
either filed by a user from the dashboard (source: "user") or
automatically by call grading (source: "system"). Non-blocking.
{
"data": {
"call_id": 987654321,
"issue_report": {
"created_at": "2026-04-20T18:25:11.002Z",
"description": "Five-second silence before responding to the main question.",
"id": 4321,
"severity": "warning",
"source": "system",
"status": "open",
"title": "Agent paused too long"
}
},
"event_id": "5e6f7a8b-9c0d-4e1f-8a2b-3c4d5e6f7a8b",
"type": "issue.reported"
}| Field | Type | Description |
|---|---|---|
agent_id | integer | null | Agent that handled the call, when one was assigned |
agent_name | string | null | Agent that handled the call, when one was assigned |
issue_report.severity | string | critical, warning, or info |
issue_report.status | string | open or resolved |
issue_report.source | string | user (filed from the dashboard) or system (created by grading) |
issue.escalated
Sent when an issue pattern is sent to ThunderPhone for staff review: after Report to ThunderPhone, or when Fix with AI cannot confirm a customer-side fix and routes the issue automatically. Only organization-wide endpoints receive this event.
{
"data": {
"automatic": false,
"cluster_id": "7ac2844c-2df0-4fa8-a560-7378da649e19",
"escalation_id": "ec99f52b-c8c0-41dd-a4f2-dd8a07b10894",
"status": "open"
},
"event_id": "d8f8f420-7a42-45ba-bcf1-b747f9bbecda",
"type": "issue.escalated"
}| Field | Type | Description |
|---|---|---|
automatic | boolean | true for an automatic escalation; false for a manual escalation |
cluster_id | UUID | Escalated issue pattern |
escalation_id | UUID | Escalation record |
status | string | open when the event is emitted |
This is a notification, not the evidence bundle. Use cluster_id to correlate
it with the issue pattern. See Report to ThunderPhone.
Test call events
test-call.completed
Sent when a
test-call run
reaches a terminal status — completed or failed, including runs
that failed at launch and never produced a call. Non-blocking. Useful
for wiring batch CI runs into your chat/notifications systems.
{
"data": {
"test_call_run": {
"call_id": 987654321,
"completed_at": "2026-04-20T18:25:04.822Z",
"error_message": "",
"id": 7110,
"status": "completed",
"target_id": 12,
"target_type": "agent"
}
},
"event_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
"type": "test-call.completed"
}| Field | Type | Description |
|---|---|---|
agent_id | integer | null | Agent that handled the call, when one was assigned |
agent_name | string | null | Agent that handled the call, when one was assigned |
test_call_run.target_type | string | agent or phone_number |
test_call_run.target_id | integer | The agent id or phone-number id the run targeted, matching target_type |
test_call_run.status | string | completed or failed |
test_call_run.call_id | integer | null | null when the run failed before a call was placed |
test_call_run.error_message | string | Empty on success |
Alert events
alert.triggered
Sent when an alert rule with the Deliver to developer webhooks channel enabled crosses its threshold. Non-blocking. A rule fires once and then respects its cooldown, so a sustained breach produces one event per cooldown window.
{
"data": {
"comparator": "lt",
"event_id": "b8e6a1d4-2c3f-4a5b-9c8d-7e6f5a4b3c2d",
"fired_at": "2026-04-20T18:00:00+00:00",
"metric": "success_rate",
"metric_value": 71.4,
"rule_id": "d2c3b4a5-6f7e-4d8c-9b0a-1c2d3e4f5a6b",
"rule_name": "Success rate below 80%",
"threshold": 80.0,
"window_hours": 24
},
"event_id": "4d5e6f7a-8b9c-4d0e-9f1a-2b3c4d5e6f7a",
"type": "alert.triggered"
}| Field | Type | Description |
|---|---|---|
event_id (in data) | UUID | The alert firing id — distinct from the envelope's delivery event_id |
rule_id, rule_name | UUID, string | The rule that fired |
metric | string | success_rate, failure_rate, avg_score, call_volume, or suite_regression |
comparator | string | lt, lte, gt, or gte |
metric_value | number | The metric's value over the window when the rule fired |
threshold | number | The configured threshold |
window_hours | integer | Trailing evaluation window |
fired_at | timestamp |
See the Alerts guide for creating rules, metrics, cooldowns, and the email / Slack channels.
Migration events
migration.call.complete
Sent when a call placed through
POST /v1/migrations/{id}/calls ends,
whichever platform took it. Non-blocking. It arrives up to about 5 minutes
after the call ends. data has every field of
telephony.complete plus a migration block. A
call ThunderPhone placed also sends telephony.complete as usual, with the
same call_id. A call on the other platform that has not ended after 2
hours, or whose migration finished or lost its key first, is sent with
status: "unknown" and no transcript.
{
"data": {
"agent_id": 42,
"agent_name": "Front desk",
"billable_minutes": null,
"billing_total_cents": null,
"call_id": null,
"direction": "outbound",
"duration_seconds": 94,
"end_reason": "user_hangup",
"end_time": "2026-10-06T17:01:34+00:00",
"extracted_data": null,
"from_number": "+15551234567",
"migration": {
"arm": "vendor",
"id": "5f0c2a9e-7b1d-4c3e-8a6f-2d9e1b4c7a30",
"metadata": {
"lead_id": "8812"
},
"routing_id": "9b2f8c1e-4d3a-4f6b-8e2d-1a7c5b9e0f34",
"vendor": "retell",
"vendor_call_id": "call_6a1f0e",
"vendor_cost_usd": "0.1240"
},
"product": "",
"recording_mode": "vendor",
"recording_url": "https://…",
"start_time": "2026-10-06T17:00:00+00:00",
"status": "completed",
"summary": "The callee booked a table for two on Thursday.",
"thunderphone_retained_call_content": true,
"to_number": "+14155552671",
"transcripts": [
{
"content": "Hi Ada, this is Rosa's calling about your booking.",
"role": "agent"
},
{
"content": "Yes, Thursday works.",
"role": "user"
}
],
"transfer_number": null,
"unresolved_variables": [],
"variables": {
"first_name": "Ada"
},
"voice": ""
},
"event_id": "7c1e2f3a-4b5c-4d6e-8f7a-9b0c1d2e3f4a",
"type": "migration.call.complete"
}| Field | Type | Description |
|---|---|---|
migration.id | UUID | The migration |
migration.routing_id | UUID | The routed call, as returned when you placed it |
migration.arm | string | ours (ThunderPhone placed it) or vendor (the other platform did) |
migration.vendor | string | retell, vapi, elevenlabs, or bland |
migration.vendor_call_id | string | null | The other platform's call id; null for ours |
migration.metadata | object | The metadata you sent when placing the call |
migration.vendor_cost_usd | string | null | What the other platform reported the call cost, when it did |
call_id | integer | null | The ThunderPhone call id; null for vendor |
status | string | For vendor: completed when the callee spoke, failed when they did not, unknown as above. For ours: as in telephony.complete |
For a vendor call, ThunderPhone has no billing or extraction data:
billable_minutes, billing_total_cents and extracted_data are null,
product and voice are empty, recording_mode is vendor, and
recording_url is the other platform's link, when it gave one.
migration.rollback
Sent when a migration's gradual switch moves every call back to the other platform on its own: 3 calls failed to connect to ThunderPhone within 15 minutes, 2 calls sent to ThunderPhone ended before the agent answered, or ThunderPhone's task outcome rate fell more than the configured gap below the other platform's. Non-blocking, once per rollback. After it, ThunderPhone's share is 0% and the migration is paused until someone resumes it on the agent's Migration tab. The payload carries no phone numbers, transcripts or call ids.
{
"data": {
"agent_id": 42,
"agent_name": "Front desk",
"dropped": 0,
"migration_id": "5f0c2a9e-7b1d-4c3e-8a6f-2d9e1b4c7a30",
"ours": {
"graded": 20,
"success_rate": 0.8
},
"ours_failed": 3,
"ours_percent": 0,
"previous_percent": 25,
"reason": "ours_failed_burst",
"rolled_back_at": "2026-10-10T12:05:00+00:00",
"step": 1,
"theirs": {
"graded": 20,
"success_rate": 0.8
},
"vendor": "retell"
},
"event_id": "3d1f6b2a-8c4e-5a7b-9d0e-2f1a3b4c5d6e",
"type": "migration.rollback"
}| Field | Type | Description |
|---|---|---|
migration_id | UUID | The migration |
vendor | string | retell, vapi, elevenlabs, or bland |
reason | string | ours_failed_burst (3 failed connections in 15 minutes), dropped_calls (2 calls ended before the agent answered, within 30 minutes), or rate_gap (task outcome rate gap over at least rollback_min_calls graded calls per side) |
previous_percent | integer | ThunderPhone's share of calls before the rollback |
ours_percent | integer | Always 0 |
step | integer | null | The schedule step it was on (0-based) |
ours / theirs | object | null | Graded calls in the current step and their success rate; theirs may be the last 30 days when the step has too few calls. null when there were none |
ours_failed | integer | Failed connections counted in the last 15 minutes |
dropped | integer | Calls counted as dropped in the last 30 minutes |
rolled_back_at | timestamp |