Post-call results

Verify signed lifecycle and artifact events, then optionally read a bounded advisory summary from a completed call.

Use signed lifecycle events to reconcile what OpenPhonex did, and use the call evidence endpoints when you need the recording, transcript, or artifact itself. An optional post-call extraction is a convenience summary derived after a call; it is never the authoritative billing, policy, or customer-system record.

Configure an event endpoint

In the Workspace, configure the agent/webhook endpoint and use Send test event before relying on it. The test travels through the real delivery pipeline and appears in the delivery feed.

The REST routes are POST /v1/agent-endpoints, GET /v1/agent-endpoints, PATCH /v1/agent-endpoints/{agent_endpoint_id}, and GET /v1/webhook-deliveries; lifecycle controls are POST /v1/agent-endpoints/{agent_endpoint_id}/pause, /resume, and /test-event. The endpoint creation schema includes your webhook_url and audio_stream_url; save the returned webhook_secret immediately. It is revealed once and redacted on later reads. See the generated API reference for the exact request schema and scopes.

events is set once at creation. Omit it (or send an empty array) to get the default subscription:

["call.created", "call.answered", "media.started", "call.ended", "call.completed", "call.failed", "call.canceled"]

Change it later with PATCH /v1/agent-endpoints/{agent_endpoint_id} — send the complete replacement list; an empty array is rejected there, since it would otherwise mean "subscribe to everything." The catalog also includes these useful call/result signals you can add to the subscription:

EventUse it for
call.created, call.queued, call.dialing, call.answered, media.startedLive lifecycle tracking
call.ended, call.failed, call.completed, call.canceledTerminal reconciliation
call.transcript.ready, call.recording.readyFetching newly available evidence
call.extraction.ready, call.extraction.failedReading optional advisory extraction status

call.canceled is terminal like call.failed: the call was cancelled before it reached the carrier, so no recording or transcript will follow and any reserved balance is released. Treat it as a stop condition wherever you wait for call.completed.

GET /v1/webhook-deliveries is the scoped delivery feed: it shows each event, endpoint, retry status, and safe failure evidence. It is the operational view when an endpoint needs attention.

Verify every delivery

OpenPhonex sends a canonical JSON envelope and the same HMAC headers described for Integration Actions:

{
  "id": "evt_123",
  "type": "call.completed",
  "created_at": "2026-08-13T12:00:00Z",
  "organization_id": "org_123",
  "data": {
    "call_id": "call_123",
    "external_reference": "case_456"
  }
}

Verify X-Agent-Telco-Timestamp and X-Agent-Telco-Signature over the exact raw body before parsing it. Reject stale or invalid signatures, persist the event id, and make your event handler idempotent. Deliveries are retried with backoff, so the same event can arrive more than once. A successful response from your endpoint is an acknowledgement, not permission to skip deduplication.

Use terminal call events and the evidence endpoints as the OpenPhonex outcome record. The data body contains result context; fetch the scoped resource when you need the full artifact instead of relying on event bodies as a data export. GET /v1/calls/{call_id} (using data.call_id from the event) is the authoritative fetch for the call itself after a terminal event — status, provider call ID, your own external_reference, and lifecycle timestamps.

Every call.failed result and call row returned by GET /v1/call-results includes a bounded technical_failure object. Evidence is available only for calls created after this contract was introduced; historical and unclassified failures return the same object with unknown availability rather than being reinterpreted:

{
  "evidence_version": 1,
  "availability": "available",
  "terminal_status": "no_answer",
  "failure_reason_code": "outbound_ringing_timeout",
  "failure_stage": "outbound_sip_participant",
  "sip_status_code": 487
}

Treat this as structured technical evidence, not a business outcome. The fields come from a fixed OpenPhonex vocabulary; raw provider error text, SIP headers, exceptions, phone numbers, transcripts, credentials, and stack traces are not included. sip_status_code is nullable. When OpenPhonex cannot prove a safe cause, availability and terminal_status remain unknown rather than being inferred from call duration or today’s configuration.

Only final SIP responses from 200 through 699 are retained in this terminal failure field. Provisional call-progress responses such as 180 Ringing are not terminal evidence and are omitted.

no_answer is narrower still: it is published only when a versioned durable outbound_ringing_timeout event proves it. A bare SIP 487 or elapsed duration does not prove no answer. In the example above, the ringing-timeout reason—not the accompanying 487—is the authoritative evidence. This contract applies prospectively; older failures are not backfilled or reclassified. The exact allowlisted values and pair constraints are defined by the OpenAPI schema.

Read evidence and optional extraction

After the relevant ready event, use the generated reference routes for:

  • GET /v1/calls/{call_id}/recording
  • the recording metadata's download_url for authenticated full or byte-range audio
  • GET /v1/calls/{call_id}/transcript
  • GET /v1/calls/{call_id}/waveform
  • GET /v1/calls/{call_id}/artifacts
  • GET /v1/calls/{call_id}/post-call-extraction

Each read remains scoped to your organization and API key. Store only the evidence your policy and retention rules permit.

End a call that is still running

POST /v1/calls/{call_id}/cancel withdraws a call that is still queued, before anything dials. Once a call is live that is no longer the right tool, and it fails closed rather than implying the carrier call was stopped.

POST /v1/calls/{call_id}/hangup is for a call that is already live. It answers 202 with a hangup_requested_at timestamp and deliberately does not report the call as ended: the media session is torn down by the worker holding it, and the call then reaches call.completed through the ordinary path. Ask twice and you get the original request back rather than a second teardown.

Until the teardown is acknowledged the request stays visible with status pending, so a request that nothing acted on is something you can see rather than silence. If a teardown fails, the call is still up, so asking again starts a fresh attempt; after three consecutive failures the endpoint refuses so the call is escalated rather than retried.

A call still queued for origination is refused with a pointer to cancel, and a call that has already finished is refused too.

Why a call ended

A completed call carries ended_reason on the call resource and in its terminal webhook:

ValueMeaning
remote_party_disconnectedthe other party hung up
hangup_requestedsomeone ended the call from your workspace or through the hangup endpoint
max_call_duration_reachedthe call reached its configured maximum length
no_caller_speech_after_answerthe call was answered but nobody ever spoke
agent_completedthe agent finished the conversation and ended the call
media_session_lostthe media session went away while the call was still live

That is the whole set. The field is empty on a call that has not completed, and on calls that completed before this shipped. A call that failed reports its reason in technical_failure.failure_reason_code instead; the two vocabularies are separate and do not overlap.

When the media session is lost

media_session_lost is the one reason that names something nobody chose. It means the media session underneath a live call went away: the room was removed, or the connection carrying the audio failed. Nobody hung up, no limit was reached, and the agent did not finish.

It reports what was observed rather than guessing at a cause. In particular, if someone had just asked to end that call, the call is not reported as hangup_requested. The hangup request is answered separately, and its answer says the call was already gone. A reason that named a person who did not end the call would be worse than the empty reason this replaces.

Calls that end this way used to carry no reason at all, so if you are counting reasons over time you will see empty values become media_session_lost from this release rather than a new kind of ending appearing.

Test calls run from the Simulations Lab can also report it. A synthetic room sometimes closes before the synthetic caller's own departure is delivered, which is the same observation from the platform's point of view. A Lab result is judged on its assertions, not on this field.

When a call ends automatically

Most calls end because someone hung up, or because the agent finished what it had to say. Both are reported now. A call the other party hangs up on carries remote_party_disconnected, and one the agent ends itself — its script reaches a goodbye, or its workflow completes — carries agent_completed. Both were empty before this release, so from here most completed calls carry a reason rather than none. In a scripted workflow agent_completed will usually be the commonest of all.

When the agent ends the call itself, its closing line is now played to the end before the call is torn down. OpenPhonex learns that a line has finished playing slightly before the other party has actually heard it, and hanging up at that moment clipped the last word or two. The call is instead held open until the audio going to the caller has fallen quiet. That wait is short and bounded: it gives up after about a second and ends the call anyway, so a closing line that never finishes cannot hold a call open. Where the audio could not be observed, the ending is recorded as unconfirmed rather than as heard.

Beyond those two, OpenPhonex ends a live call by itself when one of the following applies, so a call cannot run unattended against your balance. These are safety limits rather than ordinary endings: they exist for a call that is going nowhere. Each reports the matching ended_reason above. The maximum length and the silence window can be set per agent, as described under Changing the values below; the empty-line check uses a platform value that is the same for every agent.

Nobody is left on the call. The call ends once the agent is the only one on it. Normally the network reports the moment the other party leaves. As a backstop OpenPhonex also asks the media server who is on the call every 15 seconds and ends one that has been empty for 15 seconds continuously, so a notification that never arrives cannot leave an agent talking to nobody. Both routes report remote_party_disconnected, because they are the same ending; by the backstop the call ends within about 30 seconds of the line going empty.

The call reached its maximum length. A call ends after 29 minutes by default and reports max_call_duration_reached. The limit sits just under the 30 minute ceiling of the underlying media session, so the call ends with a stated reason rather than being cut off without one.

Nobody spoke after the call was answered. If no speech is heard from the other party for 20 seconds after the call is answered, by default, the call ends and reports no_caller_speech_after_answer. Speech is detected from the audio itself rather than from a transcript, and once the other party has been heard once the rule does not apply again for the rest of that call. This measures audio, not intent: a voicemail greeting is speech, so this rule is not a voicemail detector. What it catches is a call that connects to silence.

A hangup you request yourself is not automatic either, and it reports hangup_requested. Ask before the call has started ending and that is what you get; ask once the agent's own ending is already under way and the call keeps agent_completed, because by then it was finishing anyway. If the teardown fails the request is reported back to you as failed rather than retried on your behalf, and asking again starts a fresh attempt.

What you are billed

A call ended by one of these rules is billed exactly like any other completed call. Nothing extra is charged for the ending itself, and because the call stops sooner than it otherwise would, these rules can only reduce what a call costs.

Changing the values

The maximum length and the silence window are per-agent settings. Set them on the agent's turn-taking configuration with PATCH /v1/agents/{agent_id}/voice-stack, or the equivalent MCP tool:

SettingDefaultRange
max_call_seconds1740, that is 29 minutes60 to 1740
no_caller_speech_seconds2010 to 120

A value outside its range is refused, not quietly adjusted, so a setting the API accepted is the setting your call runs. An agent that sets neither uses the defaults above, and existing agents are unaffected.

Each range has a reason worth knowing before you pick a value. max_call_seconds cannot exceed its default, so this setting can only make a call end sooner: the media session behind a call is dropped at 30 minutes, and a limit above the ceiling could never fire, which would leave the call ending with no reason attached. no_caller_speech_seconds has a floor because the platform listens for voice activity rather than for words, so somebody who is merely listening looks the same as somebody who is not there; a shorter window would end the call partway through the agent's own opening line.

The empty-line check is not settable. If its timing does not suit your use case, contact support rather than working around it in your agent.

Reconcile a bounded external call group

If your application supplied the same opaque external_group_reference when creating several calls, read that one group with GET /v1/call-results. This is a factual, read-only reconciliation view: it does not retry recipients, create calls, classify a business outcome, or contact a carrier.

GET /v1/call-results?organization_id=org_123&project_id=proj_123&external_group_reference=import_2026_09_01&limit=50

The key is meaningful only to your application. It must be inside the authenticated organization and project, and OpenPhonex does not give it campaign, collection, retry, or business-state semantics. The route requires both calls:read and recordings:read because its evidence pointers can lead to authenticated recording or transcript resources.

The response keeps authoritative technical call facts separate from optional advisory extraction. It contains only bounded, text-free evidence anchors (turn IDs and time ranges), not a transcript or audio copy. Open the authenticated per-call recording or transcript route when your own review policy needs the source material. If evidence.availability is unavailable, the response could not assemble that optional projection; it does not prove that an underlying recording or transcript is absent. Retry the authenticated per-call evidence route rather than treating it as a business outcome.

Each call row also contains the versioned technical_failure projection described above. Persist its evidence version and treat unknown as a valid answer. Do not reconstruct a cause from transcript text, duration, or provider messages in another field.

Use the returned pagination.next_cursor while pagination.has_more is true; a page is not a complete group. You can narrow a page by terminal status, artifact readiness, extraction state, or generic platform-quality exception. Those filters never apply your business taxonomy.

The group publishes answered_or_voicemail with its numerator and denominator because carrier/media answer evidence can include voicemail. It does not label that value a human-answer or connect rate. Until your application has its own validated signal, human_answered and voicemail are explicitly not_provable.

For optional extraction, schema_version identifies the immutable customer schema used for that call, while analysis_version identifies the OpenPhonex-owned advisory evidence envelope. analysis_version is snapshotted when the extraction is enqueued: a later OpenPhonex analysis release never relabels an existing call's result, although newly enqueued work can use a newer version. Preserve both when you store a reference to an extraction. Handle call.extraction.ready and call.extraction.failed idempotently, then reconcile against this read or the single-call endpoint before taking an application-owned action.

Review conversation languages

In Calls, select a call and open its language history above the transcript. For supported Follow-caller sessions, the imported call evidence records applied, rejected, and failed language changes, their source, and the published configuration version used by the call. A queued request is not proof that the AI agent changed language.

The history describes the AI agent's response-language configuration. It does not label every caller utterance or certify which language was spoken in the audio. Listen to the recording and read the transcript to check the actual conversation. An explicit-request source identifies the controlled switch-tool path; provider-evidence identifies the supported language-detection path.

Language history is post-call evidence, not a live monitoring feed. It becomes available after the worker's telemetry artifact has been imported. A partial history means the worker could not capture the complete sequence, including when its event limit was reached. Unavailable means no supported history has been imported; older calls are not reconstructed from today's agent settings. An available history with no changes is different from missing evidence.

Times are relative to the worker's call clock. They are not automatically recording seek positions: a recording can start on a different clock. The UI does not invent recording offsets or transcript language labels from these events. Language evidence does not replace the call's billing or terminal status records.

Optional post-call extraction

Configure a hosted agent with PATCH /v1/agents/{agent_id}/post-call-extraction. The request has enabled and a JSON schema. When enabled, the schema must be an object with 1–24 scalar properties (string, number, integer, or boolean), optional string enums, and no additional properties.

{
  "enabled": true,
  "schema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "requested_callback": { "type": "boolean" },
      "follow_up_reason": { "type": "string" }
    }
  }
}

Every configuration edit creates an immutable version for future calls. After a completed call has a transcript, a separate worker may produce a result. Read it through GET /v1/calls/{call_id}/post-call-extraction; its status is one of not_configured, pending, running, ready, or failed.

Advisory, not an action

Extraction never changes a call, policy decision, wallet, customer record, or integration. It can support review and triage, but your application must verify the source evidence before it performs a business action.

Campaign results carry the extraction too: the results export writes one extraction.<field> column per schema field beside the call's workflow outcome, and the review inbox shows the same fields inline for every call that asked for a person.

Security and reliability checklist

  • Keep the reveal-once webhook secret in your secret manager; rotate by creating a new endpoint if it is lost.
  • Verify raw-body HMACs and freshness before processing any event.
  • Deduplicate by event id and reconcile terminal status with call evidence.
  • Pause a failing endpoint, repair it, send a test event, then resume it.
  • Keep post-call extraction schemas short, neutral, and reviewable; do not use them as a substitute for a verified customer decision.

On this page