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 Skysay 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. An agent created with
POST /v1/agents gets its own endpoint, and that response reveals its secret
the same way, once, in agent_endpoint.webhook_secret. 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
Skysay 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 Skysay 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 Skysay vocabulary; raw provider error text, SIP headers,
exceptions, phone numbers, transcripts, credentials, and stack traces are not
included. sip_status_code is nullable. When Skysay cannot prove a safe
cause, availability and terminal_status remain unknown rather than being
inferred from call duration or today’s configuration.
A call refused before anything dialled reports terminal_status: "failed" and
failure_stage: "outbound_authorization", with failure_reason_code
outbound_authorization_rejected (refused by your workspace capabilities) or
outbound_authorization_held_for_review (held for review; its call status
stays needs_review).
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.
worker_entry_failed (stage worker_entry) is the one reason Skysay's own
media worker publishes. It means the platform ended the call itself: the call
was admitted, and then the worker failed before it reported that media had
started, so the person who answered would otherwise have been left listening
to silence. Skysay hangs up that phone leg, closes the room, records this one
call.failed result and releases the wallet reservation; nothing is charged
for the call. There is no setting that changes this, and no override: a call
that never delivered media is never continued. sip_status_code is null on
this reason, because the phone leg itself did not fail. Retry policy treats it
as a technical failure on Skysay's side, not as a busy or unanswered contact.
platform_maintenance (stage inbound_admission) means an incoming call
arrived during a scheduled Skysay maintenance window. Skysay does not answer
it, records the call as failed once, and charges nothing. It is not caused by
your workspace's settings; see
Maintenance windows.
inbound_capacity_reached (stage inbound_admission) means an incoming call
arrived while every agent line on the platform's phone host was already in a
call. Skysay does not answer that call: the caller hears a busy signal straight
away, instead of ringing with nobody to pick up, and the call is recorded
failed, once, with nothing charged. sip_status_code is null on this reason.
A number's rule fallback is not applied to these calls yet, so the call is
not forwarded anywhere.
An outgoing call never gets a busy for this: when every agent line is in a call
as an outgoing call is about to be dialled, Skysay does not dial it. The call
goes back to its queue and waits for a free line, and waiting spends none of
its attempts. It is dialled once a line frees, and every check that applies
to a call is made again at that dial: your do-not-call register, the
campaign's state and the contact's calling window. If no line frees for about
ten minutes, the call ends outbound_capacity_unavailable (stage
outbound_worker): it was never dialled, nothing is charged, and
sip_status_code is null.
When the configuration could not be run
Five reasons share the stage component_configuration. They mean the call was
admitted and then refused before any audio existed, because the agent's
resolved configuration could not build a conversation. Four of them name the
missing piece:
| Value | Meaning |
|---|---|
voice_id_missing | no voice is chosen, so the agent has nothing to speak with |
tts_model_missing | no speech model is chosen, so replies cannot become audio |
llm_model_missing | no language model is chosen, so the agent has nothing to think with |
system_prompt_missing | no instructions are written, so the agent does not know what the call is for |
The fifth, component_configuration_rejected, is the general case: the
configuration was refused for a reason outside that list — an unavailable
provider or model for the call's language, a voice that is not in the approved
inventory, or a language the agent is not published to answer in.
What the four name is the field, never its contents: system_prompt_missing
tells you instructions are missing and never quotes them. As with every
pre-media refusal, Skysay hangs up the phone leg, closes the room, records one
call.failed result and releases the wallet reservation, so nothing is charged.
sip_status_code is null: the phone leg itself did not fail.
These are configuration problems, not faults, and retrying the same call without changing the agent produces the same result. The agent's page in your workspace reports the same missing pieces before you place a call, and a browser test against an agent in this state is refused rather than connected. Calls that failed this way before this release carry no reason at all; they are not backfilled.
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.
In the workspace, the fields a post-call extraction pulls out are set on each AI agent, in its After the call section, beside the free AI call summary; see After the call.
When a recording arrives late, or not at all
Skysay stores the recording, the transcript and the call's evidence for calls
of any length up to the maximum call length.
It stores them after the call ends, normally within moments of the hangup, and
sends call.recording.ready and call.transcript.ready once they are
available.
Storing can fail for a reason that a later attempt can outlive, such as a
network fault or a platform update during the hangup. In that case Skysay
retries on its own, for about five hours. The gaps between attempts grow: 1,
5, 15, 60 and then 240 minutes. The ready events are sent when a retry
succeeds, so they can arrive hours after call.completed. Each is still
delivered once. Until then, the call has no recording or transcript to read.
If storing is refused for a reason a retry cannot change, or the retries run
out, the call's own event log (GET /v1/calls/{call_id}/events) records a
call.artifacts.failed row with a reason and the stage that failed. No
webhook is sent for it. A retry never changes what the call costs: the call is
billed from its lifecycle when it ends, not from the stored recording.
Transcripts of GPT-Live calls
GPT-Live 1 listens and speaks at the same time, so the agent often starts talking before the caller has finished. Its transcript records what each side said, not a strict question-and-answer sequence:
- Every line the agent spoke is a turn, including the opening and any line
spoken while the caller was still talking. An agent turn is placed where its
audio started, so it can begin before the caller turn it overlaps has ended.
Its
metadata.text_statusis normallymodel_output_transcript: the text is the engine's own transcript of what it said. - A stretch of agent audio that the engine did not transcribe has no turn. It is still in the recording.
- If the engine transcribed a line but its audio never played, the turn's
metadata.actually_spoken.audio_observedisfalse. - If the call ends while the caller is still speaking, their last words are
kept as a turn with
metadata.text_statusset tofinalized_at_close. The engine had not finished transcribing that utterance, so the text can stop mid-sentence.
A call where only the opening played has a transcript with that one agent
turn, as a Standard call does. It sends call.transcript.ready, and post-call
extraction runs on it if the agent has an extraction schema.
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.
Hang up from the workspace
The Hang up button on a live call's detail in the Calls page sends this same request after a confirm step; see End a live call from Calls.
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 |
funding_exhausted | the current credit reservation could not be extended before its funded deadline — see Funding a running call |
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 |
no_human_after_answer | the call was answered but only music or tones were ever heard |
model_session_lost | the conversation engine behind the call failed and could not be recovered |
agent_unresponsive | the other party spoke and the agent produced nothing for the whole of its bound — see When the agent stops answering — or, with an empty reason_detail, the agent's opening could not be produced and never reached the caller (details) |
caller_unresponsive | both sides fell silent after the agent spoke, the agent asked whether the caller was still there, and nobody answered — see How a call ends |
ivr_timeout | an outbound call that was in a phone menu or a hold queue ended there: a limit ran out, the menu had no way to a person, or it asked for a value the call did not carry; reason_detail says which. Phone menu navigation is on by default, so any call your AI agent places can end this way |
listener_unavailable | a workflow agent's speech listener (its speech-to-text provider) failed while the caller was on the line, so the agent could no longer hear them — see When a workflow's listener fails |
workflow_line_undelivered | a workflow agent could not deliver one of its lines before the end of the workflow, so the workflow could not go on — see When a workflow line is not delivered |
The workspace Calls detail explains funding-related endings. Read the full
ending-reason vocabulary from the call resource or the call.completed webhook.
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.
A call also carries answer_classification and reason_detail. answer_classification says what
was heard after the call was answered rather than why it ended. reason_detail is the qualifier,
and it qualifies one of the two, which you can tell from its value:
reason_detail | qualifies | meaning |
|---|---|---|
caller_tune, hold_music | answer_classification non_human_audio | continuous music or tones held the line |
looping_recording | answer_classification non_human_audio | a recording repeated the same cycle of things over and over |
no_audio | answer_classification silent | the line answered and nothing at all was heard |
model_unresponsive | ended_reason agent_unresponsive | the agent stopped answering: see below |
insufficient_balance | ended_reason funding_exhausted | the latest renewal confirmed insufficient eligible credit |
renewal_unavailable | ended_reason funding_exhausted | additional funded time was not confirmed by the deadline |
hold_limit, navigation_limit, deadline, menu_no_path, data_not_authorised | ended_reason ivr_timeout | the time allowed on hold, the time allowed in menus, or the call's overall maximum length ran out while the call was waiting; or the menu offered no way to a person; or it asked for a value the call did not supply. See How these calls end |
during_hold | ended_reason remote_party_disconnected | the other party hung up while the call was waiting in a menu or on hold |
recorded_message_only | ended_reason no_human_after_answer | a recording and nothing else answered: no menu, no person |
navigation_detector_lost | ended_reason media_session_lost | the listening a waiting call depends on stopped delivering |
It is empty when neither needs qualifying, which is most calls. no_human_after_answer is the
ending that goes with the non_human_audio classification, and only an agent set to end_call
ends a call that way. See Answer detection for the whole set, for the
two situations that end a call, and for the per-agent setting.
A wait that ran out after a conversation
The call resource also carries person_reached: true when a person was reached earlier in the
call, false when nobody was, and null when the call did not report it (a call that completed
before the field shipped, an agent that does not handle phone menus, and any ending the platform
did not decide). It matters for the three endings that mean "the call waited and nobody came":
ivr_timeout with hold_limit or deadline, and remote_party_disconnected with during_hold.
person_reached | campaign disposition | pick-up and talk time |
|---|---|---|
false | no_answer: the contact is dialled again | the call is not a pick-up and its recording is not talk time |
true | connected_completed: terminal, the contact is not dialled again | the call is a pick-up and its recording is talk time |
null | connected_completed: missing information never starts a redial | excluded, as it was before the field existed |
A conversation followed by a transfer into a queue that ran out of time has reached its contact, so
it is neither redialled nor discounted. ivr_timeout with navigation_limit, menu_no_path or
data_not_authorised is terminal whatever person_reached says, and no_human_after_answer is never
a pick-up. The same field is in the call.completed webhook, and the terminal call.ended event's details carries it as a boolean when the platform reported one.
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 the conversation engine is lost
model_session_lost is the engine-side counterpart of media_session_lost.
The media session was fine and the other party was still on the line, but the
conversation engine behind the call — the realtime model's connection to its
provider, or a speech component that ran out of retries — failed and could not
be brought back. Nobody hung up, no limit was reached and the agent did not
finish. Skysay ends the call at that moment rather than leave the line open,
with nobody on it, against your balance.
Before this release such a call stayed open until its maximum length or until
the fault check below settled it, and carried no reason. As with
media_session_lost, a hangup request that was in flight is answered
separately, and the call is not reported as hangup_requested.
When a workflow's listener fails
An agent that runs a workflow hears the caller through its speech listener, the speech-to-text provider the workflow is set to use.
If the listener's connection fails (it never connects, or its stream closes or fails), Skysay reconnects it once per call. The new connection is sent everything the caller said in their current turn, from its first word, so nothing is lost, and the call carries on as normal. The reconnect has about 3 seconds after the caller stops speaking to deliver their words.
Skysay ends the call with ended_reason listener_unavailable when the agent
can no longer hear the caller:
- the reconnect also fails, or does not deliver the caller's words in time;
- the listener fails a second time on the same call (only one reconnect is made per call);
- the listener fails for a reason a new connection cannot fix, such as a rejected configuration or a stream that reached its time limit;
- the caller's current turn is more than about 26 seconds long when the connection fails, because only the last 26 seconds of audio can be resent.
Audio resent on a reconnect can be counted again in the call's speech-to-text usage: at most the part of one turn the failed connection had already received, at most 26 seconds, once per call. If the first connection never opened, nothing was sent to it and nothing is counted twice.
When a call ends this way, nobody hung up, no limit was reached and the
workflow did not finish. Such a
call used to carry no reason, and its workflow read as if the caller had hung
up. The call's workflow.status is still incomplete, because the workflow
stopped before it finished; it did not fail on its own.
Anything the caller said that the listener did not deliver is not in the transcript, and that includes a request to stop calling. Before you dial a contact again after this ending, check the call's recording.
When a workflow line is not delivered
A spoken workflow only moves on from a step once the caller has heard its line.
Skysay ends the call with ended_reason workflow_line_undelivered when a line
before the end of the workflow could not be delivered:
- On GPT-Live 1, the line was played but GPT-Live did not say it in full (see
how GPT-Live speaks a workflow). The workflow run's
fail_reasonisLINE_NOT_SPOKEN. - On any engine, the caller spoke over the same step three times while its line
had still not been heard in full. Words said over a line that was not heard
are never taken as an answer to it, so the step is spoken again, at most
twice; the third time the workflow stops. Only a line that had begun
playing counts: words said before a repeat started playing do not. The
run's
fail_reasonisUNHEARD_REPEATS_EXHAUSTED. A stop request, a wrong-person reply and your workflow's other global interrupts are still recognised on every one of those turns, exactly as before.
Nobody hung up and no limit you set was reached. The call's workflow.status
is failed: the workflow could not go on. Such a call used to carry no reason,
and its workflow read as if the caller had hung up. Check the recording for
what the caller heard and said before you contact them again.
When the agent stops answering
model_session_lost is reported only when the engine tells Skysay it has
failed. An engine can also simply stop answering without reporting
anything — a provider outage in the middle of a call can leave the connection
open and silent — and a call in that state used to stay up, with the caller
talking to nobody, until it reached its maximum length. It was billed for all
of it.
Skysay bounds that. After the other party finishes speaking, the agent has a
fixed amount of time to produce something: a reply, a tool call, or the end of
the call. A reply counts even if the agent held it back rather than talk over the other
party and it was never played because they kept talking, so a caller who talks
in short bursts is never ended this way. If the
whole bound passes with nothing at all, Skysay ends the call with ended_reason agent_unresponsive and reason_detail
model_unresponsive. It says what was observed — an agent that went quiet with
its engine still connected — and not that anything failed, because some
engines report nothing when they stop answering.
Calls that ended this way before 2026-09-25 reported ended_reason
model_session_lost with the same reason_detail model_unresponsive. If you
count endings over time, treat that pair as agent_unresponsive.
The bound is 20 seconds by default, and it counts only time the line was
free. It starts when the other party stops speaking, and their own speech is
subtracted from it: someone who keeps talking is never cut off mid-sentence,
while someone who pauses between attempts to get an answer still reaches the
bound. It is cleared by anything the agent does that the other party can
tell is happening -- a reply they can hear, a tool call -- so a normal
conversation never approaches it, and it never runs while one of the agent's
tools is still running, before the other party has said anything at all, or
on a call whose replies come from a workflow. "The agent is speaking" means
audio actually reaching the line, not an engine that says it is speaking: an
engine that has stopped sending anything is exactly what this bound is for.
A call where nobody
speaks at all is a different situation with its own reason,
no_caller_speech_after_answer.
You can change the bound per agent with voice_polish.model_unresponsive.seconds
on PATCH /v1/agents/{agent_id}/voice-stack: any value from 10 to 120 seconds,
or 0 to switch it off entirely for an agent that waits on something genuinely
slow. Leaving it unset keeps the 20-second default.
{ "voice_polish": { "model_unresponsive": { "seconds": 45 } } }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.
How the agent ends a call — the end_call tool, where its goodbye comes from on
each conversation engine, and the standing instruction every agent with
end_call receives — is described in How a call ends.
When the agent ends the call itself, its closing line is now played to the end before the call is torn down. Skysay 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, Skysay 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 Skysay 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. A call stays open while the
caller is heard, by voice, by transcript, or by the engine's own detection:
speech is detected from the audio itself by voice activity detection; on a
GPT-Live 1 or Gemini Live agent the engine's own live captions of the other
party count as well, since it hears the caller directly; and on a Gemini Live
agent the engine starting a reply of its own to what it heard counts too (its
automatic reply to a tool result does not). 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.
Both sides went quiet mid-call (rolling out: observed but not yet
enforced; see the link below). Once the agent has spoken and nothing more
comes from either side for 30 seconds, the agent asks once whether the
caller is still there. If nobody answers within 15 seconds after that
question, the agent says a short goodbye and the call ends with
caller_unresponsive. A caller who answers, even with one word, simply
continues. The details, including when the clock does not run, are in
How a call ends.
If the software handling your call faults
A call is handled by a worker process, and that worker normally reports the ending itself. If it stops — it crashes, or it stops making progress while the call is still open — Skysay settles the call for you rather than leaving it open against your balance. Two checks do this, and neither is configurable:
- The worker stops reporting. Its status is checked every 5 seconds, and a worker silent for more than 15 seconds is treated as gone. The call is settled within roughly 20 seconds.
- The worker keeps reporting but the call is empty. Skysay asks the media server who is on the call. If the other party has been absent for 2 minutes continuously while the worker is still reporting, the call is settled anyway. The wait is deliberately longer than a healthy ending takes, so a merely slow worker is never overtaken and still gets to report its own reason.
A call settled this way is completed, and it carries the ended_reason the
worker had already decided — remote_party_disconnected when the other party
left, and so on from the table above. No new value is introduced. When the
worker faulted before deciding anything, ended_reason is empty rather than
guessed at, because nothing observed why the call ended.
The billed length is measured to the moment the call actually ended, not to the moment the fault was noticed. If the worker had decided on an ending, that instant is used, whichever of the two checks settled the call. Where it decided nothing, each check falls back to the closest boundary it can actually observe: the worker's last report when the worker went silent, and the first check that saw the other party gone when the worker was still reporting. A call that hung for half an hour after the other party left is billed to when they left.
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.
The billed duration starts when the call is placed and ends when the call ends; ringing time before the answer is included.
A call's recorded end is the moment the call ended, not the moment Skysay finished tearing it down. Those are normally a second apart. Where they are not, because a teardown was slow, the gap between them is not billed to you and does not appear in the call's duration.
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 meaning, 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?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 Skysay 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
Skysay-owned advisory evidence envelope. analysis_version is snapshotted
when the extraction is enqueued: a later Skysay 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" }
}
}
}Each scalar property can restrict its evidence with "x-skysay-source": "caller".
The default, "conversation", uses both speakers. Caller fields receive only
captioned caller speech, so an agent's recap or an unspoken form value cannot
supply the answer. A bare “yes” cannot establish a value mentioned only in an
omitted agent question; that value remains unknown. Choose caller fields for
contact preferences and facts the caller actually states. A caller-only summary
is deliberately one-sided and may omit context from the agent's questions.
{
"lender": { "type": "string", "x-skysay-source": "caller" },
"call_outcome": { "type": "string", "x-skysay-source": "conversation" }
}The annotation belongs on individual scalar properties, not the root schema
or nested objects. Other source values are rejected. Skysay uses one bounded
transcript read and at most two model requests per worker attempt, one per nonempty
source group. Normal worker retries repeat these requests. With no captioned caller words, caller fields return null without
a model request. If either model request fails, the extraction follows the
normal retry/failure process; a partially successful result is never published.
The result remains an AI estimate, not verified caller identity or consent.
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.
Languages and multilingual calls
Understand opening language, fixed-language calls, controlled language switching, and the different behavior of GPT Live and Gemini Live.
Call summaries
A free AI summary of every call with a conversation, whether a follow-up is needed, whether the agent's goal was met, and the other party's sentiment.