ThunderPhone 2.0 is live.Self-serve, from 2¢/min.Read the announcement

Webhooks

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_id to dedup on.
  • Blocking exchanges run only on the legacy single-URL webhook: the telephony.incoming / web.incoming configuration 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"
}
FieldTypeDescription
agent_idinteger | nullAgent that handled the call, when one was assigned
agent_namestring | nullAgent 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.

FieldTypeDescription
agent_idinteger | nullAgent that handled the call, when one was assigned
agent_namestring | nullAgent 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.

FieldTypeDescription
agent_idinteger | nullAgent that handled the call, when one was assigned
agent_namestring | nullAgent 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"
}
FieldTypeDescription
agent_idinteger | nullAgent that handled the call, when one was assigned
agent_namestring | nullAgent that handled the call, when one was assigned
positionintegerThe turn's index in the call history — a stable identity for ordering
rolestringassistant (agent speech) or user (caller speech)
textstringThe turn's transcript text as known at emission time
entry_typestringThe underlying history entry type: completion (agent), or user_turn / span (caller)
start_ms, end_msintegerAudio 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.

FieldTypeDescription
agent_idinteger | nullAgent that handled the call, when one was assigned
agent_namestring | nullAgent 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".

FieldTypeDescription
agent_idinteger | nullAgent that handled the call, when one was assigned
agent_namestring | nullAgent 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.

FieldTypeDescription
agent_idinteger | nullAgent that handled the call, when one was assigned
agent_namestring | nullAgent 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.

FieldTypeDescription
agent_idinteger | nullAgent that handled the call, when one was assigned
agent_namestring | nullAgent 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"
}
FieldTypeDescription
call_idintegerThe call that was transferred
agent_idinteger | nullAgent that handled the call, when one was assigned
agent_namestring | nullAgent that handled the call, when one was assigned
transfer_outcomestringrefer_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
destinationstring | nullThe number the call was transferred to
sip_statusinteger | nullThe 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_kindstring | nullbridged only: cold_fallback (a refused cold transfer ThunderPhone connected) or warm (a warm transfer's two people after the agent left)
bridge_secondsinteger | nullbridged only: the bridged time ThunderPhone billed, in seconds
bridge_total_centsinteger | nullbridged 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"
}
FieldTypeDescription
voice.idstringCustom voice public id
voice.namestringAgent voice value in the form custom:<public_id>
voice.display_namestringOrganization-facing voice name
voice.languagestringThe clone's single language code
voice.genderstringmale, female, or an empty string
voice.statusstringready for voice.ready; failed for voice.failed
voice.failure_reasonstringEmpty on success; processing failure detail on failure
voice.created_at, voice.updated_attimestampISO 8601 timestamps
reasonstringFailure 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"
}
FieldTypeDescription
agent_idinteger | nullAgent that handled the call, when one was assigned
agent_namestring | nullAgent that handled the call, when one was assigned
grade.idintegerGrade id
grade.scoreinteger | null0–100
grade.call_outcomestringsuccess, failure, unknown, or no_conversation
grade.summarystringOne-paragraph summary
grade.detected_issuesarrayIssue strings found by the grader
grade.statusstringAlways completed — only finished runs emit
grade.grader_modelstringWhich grader produced the result (e.g. heuristic-v1)
grade.graded_at, grade.created_attimestamp
reportobject | nullThe 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"
}
FieldTypeDescription
agent_idinteger | nullAgent that handled the call, when one was assigned
agent_namestring | nullAgent that handled the call, when one was assigned
summarystring | nullThe 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.statusstringAlways completed for this event
extracted_data.fieldsobjectValues keyed by the configured extraction field keys; unavailable values are null
extracted_data.evidenceobjectEvidence 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.verificationstringverified 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_reasonsobjectReasons keyed by fields that were nulled by structural grounding or the independent verifier
extracted_data.schema_versionstringHash of the exact field schema used for this extraction
extracted_attimestampISO 8601 completion time
modelstringModel 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"
}
FieldTypeDescription
agent_idinteger | nullAgent that handled the call, when one was assigned
agent_namestring | nullAgent that handled the call, when one was assigned
issue_report.severitystringcritical, warning, or info
issue_report.statusstringopen or resolved
issue_report.sourcestringuser (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"
}
FieldTypeDescription
automaticbooleantrue for an automatic escalation; false for a manual escalation
cluster_idUUIDEscalated issue pattern
escalation_idUUIDEscalation record
statusstringopen 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"
}
FieldTypeDescription
agent_idinteger | nullAgent that handled the call, when one was assigned
agent_namestring | nullAgent that handled the call, when one was assigned
test_call_run.target_typestringagent or phone_number
test_call_run.target_idintegerThe agent id or phone-number id the run targeted, matching target_type
test_call_run.statusstringcompleted or failed
test_call_run.call_idinteger | nullnull when the run failed before a call was placed
test_call_run.error_messagestringEmpty 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"
}
FieldTypeDescription
event_id (in data)UUIDThe alert firing id — distinct from the envelope's delivery event_id
rule_id, rule_nameUUID, stringThe rule that fired
metricstringsuccess_rate, failure_rate, avg_score, call_volume, or suite_regression
comparatorstringlt, lte, gt, or gte
metric_valuenumberThe metric's value over the window when the rule fired
thresholdnumberThe configured threshold
window_hoursintegerTrailing evaluation window
fired_attimestamp

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"
}
FieldTypeDescription
migration.idUUIDThe migration
migration.routing_idUUIDThe routed call, as returned when you placed it
migration.armstringours (ThunderPhone placed it) or vendor (the other platform did)
migration.vendorstringretell, vapi, elevenlabs, or bland
migration.vendor_call_idstring | nullThe other platform's call id; null for ours
migration.metadataobjectThe metadata you sent when placing the call
migration.vendor_cost_usdstring | nullWhat the other platform reported the call cost, when it did
call_idinteger | nullThe ThunderPhone call id; null for vendor
statusstringFor 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"
}
FieldTypeDescription
migration_idUUIDThe migration
vendorstringretell, vapi, elevenlabs, or bland
reasonstringours_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_percentintegerThunderPhone's share of calls before the rollback
ours_percentintegerAlways 0
stepinteger | nullThe schedule step it was on (0-based)
ours / theirsobject | nullGraded 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_failedintegerFailed connections counted in the last 15 minutes
droppedintegerCalls counted as dropped in the last 30 minutes
rolled_back_attimestamp