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:
| Event | Use it for |
|---|---|
call.created, call.queued, call.dialing, call.answered, media.started | Live lifecycle tracking |
call.ended, call.failed, call.completed, call.canceled | Terminal reconciliation |
call.transcript.ready, call.recording.ready | Fetching newly available evidence |
call.extraction.ready, call.extraction.failed | Reading 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_urlfor authenticated full or byte-range audio GET /v1/calls/{call_id}/transcriptGET /v1/calls/{call_id}/waveformGET /v1/calls/{call_id}/artifactsGET /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:
| Value | Meaning |
|---|---|
remote_party_disconnected | the other party hung up |
hangup_requested | someone ended the call from your workspace or through the hangup endpoint |
max_call_duration_reached | the call reached its configured maximum length |
no_caller_speech_after_answer | the call was answered but nobody ever spoke |
agent_completed | the agent finished the conversation and ended the call |
media_session_lost | the 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:
| Setting | Default | Range |
|---|---|---|
max_call_seconds | 1740, that is 29 minutes | 60 to 1740 |
no_caller_speech_seconds | 20 | 10 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=50The 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
idand 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.