Read call evidence

Recordings, waveform peaks, transcript turns, summaries, and retention/disclosure state for every call.

Real browser and phone calls can attach evidence to the session. An agent reads available evidence with the call and recording tools. Failed calls and offline synthetic fixtures need not have real audio; check recording state rather than assuming every call has a recording.

A completed call with no recording

A completed call can have no recording and no transcript because no audio was captured on the line. Most often the person on the other end hung up before the greeting finished. The Calls page says so on that call: "The caller hung up before the greeting finished" on an inbound call, "The called person hung up…" on an outbound call, "The tester left before the greeting finished" on a browser test, and "No audio was captured on this call" when nothing was captured for another reason. A completed call with no recorded reason shows "No recording for this call." The call's ending and billing are unaffected.

The evidence tools

→ list_calls(...)                        # calls:read
→ get_call(call_id="cl_...")             # calls:read — status, recording state, where the workflow ended
→ get_call_recording(call_id="cl_...")   # recordings:read — metadata + disclosure
→ GET recording.download_url              # recordings:read — playable audio bytes
→ get_call_transcript(call_id="cl_...")  # recordings:read — transcript turns
→ get_call_summary(call_id="cl_...")     # recordings:read — AI summary (see Call summaries)

list_calls returns one newest-first page of call records (without the runtime_config that get_call includes). Pass pagination.next_cursor back as cursor while pagination.has_more is true to read older calls; see Bounded results and paging.

Translate a transcript

Open a call in Calls and choose Original, Translation, or Both. Choose the language you want to read. Switching the display or reading a saved translation is free and does not start processing.

If no translation exists, Translate shows the maximum charge before you request it. You can translate an individual eligible call even when automatic translation is off. Original speaker labels, turn IDs, recording timestamps, and missing-caption markers stay in place. Translations are machine-generated reading aids; use the original and recording to verify important details. They do not replace structured call outcomes or regenerate the analysis.

When you enable automatic translation or request one explicitly, Skysay sends the stored transcript or message text to Google Gemini for translation. Audio is not sent by this feature. Switching views only reads the saved result.

For automatic translation, open the AI agent's Languages settings, turn on Translate transcripts automatically, choose up to six target languages, and save translation settings. The setting is captured for each new communication. Later changes apply to future communications. Existing saved translations remain readable after automatic translation is turned off.

Translation runs on a separate background worker after the call ends and its stored transcript settles. It does not add translation inference to the live voice conversation. AI-agent-associated SMS messages use the same policy and show translations beside the original message in SMS.

You can explicitly translate an older call or message as long as you can access its retained text and the source is ready. The creation date does not prevent a translation request. Choose a language in the transcript or message viewer, check the price, and select Translate, or use the same API command below. Repeating a request for the same source revision and language reuses its result.

Automatic AI agent settings apply to new communications. They do not start a bulk translation of your history. Requesting an older source translates only the languages you request; later source corrections refresh those translations without charging again. Deleted or redacted content cannot be regenerated.

Languages, charges, and funds

Choose any language in the supported-language selector. Text already entirely in that language is preserved and is not charged. A mixed-language turn is translated as one unit so its context is preserved; only its original words that need translation are charged. Missing speech cannot be recovered by translation.

Translation costs $0.02 per 1,000 eligible original words per target language, with a $0.01 minimum per successfully translated call or message per target. It uses your usual Skysay balance. For example, translating 500 eligible words into English costs $0.01; translating those words into English and German costs $0.02 in total. Charges are rounded up to a cent. The price shown before processing is an upper bound because language detection may find some text already in the requested language. Non-USD wallets use the platform's currency conversion.

Word counts use a fixed Unicode word-boundary rule across writing systems, rather than counting spaces or estimating from audio duration. Punctuation by itself is not a word. Retries, cached reads, and replacement translations after platform transcript corrections do not produce duplicate customer charges.

If funds are unavailable, only translation waits. Add funds to resume it; you can still read the original. A failed translation leaves the original available. Removing or redacting source text also invalidates and removes its stored translation text.

API reads and explicit requests

Use the canonical API reference for the request and response fields:

  • PATCH /v1/agents/{agent_id} configures the agent's translation policy.
  • POST /v1/calls/{call_id}/transcript/translations explicitly requests a target.
  • GET /v1/calls/{call_id}/transcript?translation_language=en reads originals and the current cached English translation together.
  • The call transcript translation read endpoint and SMS translation endpoints expose the same saved artifact states. A GET never calls the translation provider or creates paid work.

Subscribe to call.translation.ready, call.translation.failed, sms.translation.ready, or sms.translation.failed for completion notices. These events contain source and translation references, the source revision, target language and status. Fetch the cached artifact through the API; the event does not include transcript text.

Transcript reads are bounded pages. Follow pagination.next_cursor while pagination.has_more is true, keeping the same target language. A page contains complete original/translation turn pairs and caption-gap markers. If the original changes during pagination, the API returns 409; restart at the first page rather than combining different transcript revisions. A source turn too large for a page has an explicit unavailable placeholder; its cursor still allows later turns to be read.

Processing accepts at most 2,000 text units and 200,000 source characters, with at most 8,000 characters in one unit and 32 chunks. Oversized sources are rejected before provider processing and are never silently translated only partway. A partial original transcript can have a translation of all available text; its missing-caption markers remain visible.

When a turn clip is unavailable

If Skysay detects delayed audio arriving in a backlog, it cannot reliably locate individual transcript turns in that recording. The Calls transcript keeps the original words and explains that clip timing is uncertain. Listen to the full recording instead. Both speakers' turn clips are withheld for that call; normal clip playback remains available for unaffected calls.

For these turns, transcript metadata.timing_quality is ambiguous, metadata.timing_reason is delivery_backlog, and audio_artifact_id is null. New recordings without detected backlog use heuristic timing with reason callback_timing: absence of detected backlog is not a guarantee of sample-accurate timing. This detection does not repair the recording's timing or reconstruct missing captions. It does not change saved historical calls.

What a recording contains

The transcript does not have to alternate between caller and AI agent. On the standard STT/LLM/TTS pipeline, consecutive recorded agent replies remain separate entries even when the caller has not spoken between them. For example, a short acknowledgement followed by the answer should show both replies. An interrupted reply can contain text that was not heard in full; check its playback evidence and the recording. Text prepared for speech that never played is not proof the caller heard it. This applies to newly recorded calls, not a repair of old transcripts.

A recording is the call's audio metadata plus:

  • Waveform peaks — about 200 loudness values across the whole call, for rendering a scrubbable waveform. Recordings saved before 26 September 2026 are recomputed to the same detail.
  • Transcript turns — speaker-attributed turns of the conversation.
  • Retention / disclosure state — how long the audio is kept and whether the required call disclosure was made.

get_call returns the call status, recording state and where its workflow ended; get_call_recording returns recording metadata, disclosure state, and a relative download_url; get_call_transcript returns the transcript turns. Fetch download_url with the same bearer credential to play or download the audio. The endpoint accepts one standard Range: bytes=... header, so browser audio players can seek and resume with 206 Partial Content; an invalid or multipart range returns 416.

The downloaded full-call recording is a listening mix. Skysay balances the caller and agent from speech-bearing sections with bounded gain and shared peak headroom, so a quiet participant is easier to hear without amplifying a silent or noise-only track. This processing does not change timing or transcript turn boundaries. The separate captured caller and agent tracks remain unchanged as the source evidence; they are storage inputs, not additional public download URLs.

Do not construct a URL from storage_key or media_url—those fields describe storage internals and are not a stable download contract. Recording reads require the recordings:read scope, which is distinct from calls:read so you can grant transcript access without call metadata, or vice versa.

Retention

Recording retention runs on the Skysay side (dry-run or execute). The disclosure state on each recording tells you whether the call was announced as recorded — surface it when you summarize a call back to a human.

The call event log

GET /v1/calls/{call_id}/events returns one bounded page of the call's own event rows, oldest first, each with its raw payload. It is the platform's internal bookkeeping log rather than a webhook replay, so it contains types that are never delivered to any webhook endpoint. Reading it needs calls:read.

call.wrong_party_signals

Written once when a call ends on a wrong-party verdict — the caller said they are not the person the agent asked for. That verdict comes from one decision model reading one caller reply and it ends the call, so this event records how much evidence it actually rested on. It is written for both routes to that outcome: the identity step's own wrong_party route, and the mid-call interrupt:wrong_party global interrupt.

FieldMeaning
route_idWhich path carried the verdict: wrong_party (the identity step's own route) or interrupt:wrong_party (the global interrupt).
from_step_idThe workflow step the verdict was reached from.
turn_indexThe caller turn the verdict landed on. 1 is the caller's first reply.
utterance_charactersNumber of characters in that reply, after trimming whitespace.
utterance_wordsNumber of whitespace-separated words in that reply.

turn_index is -1 when the turn count could not be read, and never 0 for that case, because 0 is a real turn index: an unreadable turn must not be counted among the verdicts decided on the caller's first reply, which is the number this event exists to measure.

The caller's words are not in the payload

Only the two sizes are recorded, never the utterance itself. The transcript is where speech belongs — read it with get_call_transcript. A short reply on turn 1 is the signal that a wrong-party verdict was thin; pair it with the transcript when you want to know what was actually said.

This event is readable here but is not part of the webhook catalogue, so it is never delivered to a webhook endpoint. Poll the events endpoint for it, or read it alongside the rest of a call's evidence.

Play a recording in Calls

The recording player at the top of a call in Calls is one waveform you can click, drag or tap anywhere to jump to that moment, even before the audio has started. With the waveform focused, the arrow keys move 5 seconds, Page Up and Page Down move 15 seconds, Home and End jump to the start and the end, and Space plays or pauses. A speed button switches between 1×, 1.5× and 2×.

The waveform shows audio from the AI side in teal and audio received from the other side in purple. Its colors come from verified recorded audio channels, independently of transcript text and caption timing. This includes Gemini Live: missing or late captions do not by themselves remove the person's audio color.

A split bar and the two thin lanes above the waveform show audio from both sides within that displayed time window. On a long call, one window can contain two successive turns, so this is not proof that both people spoke simultaneously. Channel colors also include background sounds, echo or generated audio from that side. They do not confirm speech, words, or the person's identity. Very quiet stretches appear as dots.

The legend shows a saved contact name when available. Otherwise, it shows Called person and the number on outbound calls, Caller and the number on inbound calls, or Tester for browser tests. A saved name identifies your contact record; it does not prove who answered. Hovering shows the time and audio source for that window.

When source tracks cannot be linked reliably to the played recording, the waveform stays neutral with Audio source colors are unavailable for this recording. This does not mean nobody spoke. Skysay can recover eligible retained recordings from their original tracks without another call or transcription. Recordings whose required tracks were deleted or never captured cannot gain verified source colors through this recovery.

Waveform colors are separate from transcript-turn clips. A turn gets a play button only when its text can be matched safely to its own recorded audio. Gemini's late captions can therefore leave a turn without a clip even when the full-call waveform correctly shows purple person audio.

On some older calls the caller's turns had no measured start and end. After those calls ended, Skysay located each turn's words on the caller's side of the recording. A turn placed with high confidence sits at the moment it was spoken. Where a turn's own clip turned out to hold other audio, that turn no longer offers turn-by-turn playback.

The player reads only the start of the file when a call opens, so its length is known before you press play. From the press until audio actually plays it shows a loading state, and if the recording cannot be loaded it says so and offers Try again.

The Calls workspace receives bounded audio-source evidence with its existing call-detail read. The public waveform endpoint continues to return mixed-audio peaks; it does not expose the workspace's per-channel projection.

Listen turn by turn in Calls

The Calls workspace keeps the mixed recording player and can also show a compact play control beside a transcript turn. It shows that control only when the bounded Calls response marks the turn has_audio; older text-only imports and turns without an observed clip do not get a broken player.

The browser never receives a recording bucket URL or an artifact identifier. Skysay resolves the linked clip through the signed-in workspace session, after applying the same recording-consent, retention, and deletion checks as the mixed recording. Deleting a recording removes turn playback as well; it does not cause historical audio to be regenerated or retranscribed.

End a live call from Calls

A call that is still live (dialing, ringing, or connected) shows a Hang up button in its detail on the Calls page. Any member of the workspace can use it on the workspace's own calls; it is not offered on a call that is still queued (cancel that one instead), on a finished call, or on a Simulations Lab run.

Hang up asks you to confirm first, because the call ends for everyone on the line, including the caller. After you confirm:

  • The button reads Ending call… and the call keeps showing as live. The request has been accepted, but the media session is ended by the worker holding the call, so nothing is reported as ended until it actually is.
  • When the call ends, usually within a few seconds, it shows its final state. Its ended reason, which you read from the call resource or the call.completed webhook (the Calls page does not display it yet), is hangup_requested, unless the call had already started ending another way (the caller hung up, or the media session was lost), in which case that ending's reason is reported instead.
  • While the button reads Ending call… it cannot be pressed, and a repeat request returns the same request rather than a second one. If the call is still live after about a minute and a half, the button is offered again so you can retry: a teardown that failed is attempted afresh, and after three failed attempts the request is refused and the call needs support.

The request is recorded in the workspace audit log with the session that made it. The button sends the same request as POST /v1/calls/{call_id}/hangup, and Post-call results lists every ended reason.

Cancel a call that has not been dialed

An outbound call that is still waiting (queued, held by the live-call gate, or held for review) shows Cancel call in its detail on the Calls page instead of Hang up. It needs a session that may place calls.

  • Cancel call asks you to confirm first. The call is then never placed, and any amount held for it is released.
  • If a worker started dialing it in the meantime, the cancel is refused and the call shows its live state: use Hang up then.

The button sends the same request as POST /v1/calls/{call_id}/cancel.

Filter calls by campaign

The Calls page has a campaign filter beside the failure filter. It lists the newest 100 campaigns that have calls, and narrows the list, its count and Load more to the calls placed for that campaign. A link to /calls?campaign_id=<id> opens the page already filtered; a campaign that is not in your workspace shows every call and says so.

Tool calls and language switches in the transcript

Between the transcript turns, the Calls workspace shows each tool the agent finished and each language switch it made, in the order they happened on the call. The caller's own turns always stay in the order they were spoken, even on a call where only some of them have a measured start.

A tool row shows the tool's name, whether it succeeded or failed, how long it took, and its offset into the call. A language switch shows the language the agent moved to and why. Both sit inline with the turns, so you can see that the agent looked something up before it answered, or that it changed language after the caller did.

Closing requests are not disconnections

For end_call, a successful tool row means the handler ran successfully. It can request a close that is still revocable. It does not mean the other person was disconnected, and the tool's duration is not hangup latency.

Calls shows separate closing receipts when the worker captured them:

  • Closing request accepted: the existing close owner accepted a pending proposal. Another invocation can join that proposal; both requests remain visible.
  • Request canceled — caller speech detected: the owner observed caller speech and withdrew that proposal. This does not certify what the person meant or that the model understood a new question.
  • Closing committed — disconnect requested: the owner passed its non-reclaimable boundary. This is still distinct from media disconnection.
  • Provider close command submitted: the native adapter handed a close command to its socket writer. Submission alone does not prove delivery or an ended call.
  • Call ended: an independent canonical call-lifecycle ending. It is not linked to a prior request unless that relationship was recorded. The actual ending reason remains separate, including a remote hangup or platform limit.

Expand Closing details for the safe reason, source and call-scoped request and proposal IDs. A shared tool reference identifies the tool definition; it does not identify one execution. Repeated requests are kept, not collapsed by tool name.

Older recordings without owner receipts say Tool succeeded — closing outcome not recorded or Tool returned error — reason not recorded. The latter cannot distinguish a refusal from a runtime error. Skysay does not reconstruct cancellation, acceptance or a reason from nearby speech or a model farewell.

This evidence arrives after the call, through recording import. A known queued or running import reads Closing details processing. Missing import state stays unavailable. A request without a retained disposition reads Closing outcome not recorded, rather than remaining pending forever.

The dedicated closing section is bounded separately from verbose worker logs: at most 200 displayed receipt moments and 64 KiB at import, keeping a bounded head and tail plus a fixed final-commit/terminal summary. Source-observed counts can exceed the retained list; partial detail and drops stay explicit. Import retries replace the validated source summary rather than adding its counters. Deleting the recording deletes its closing receipts. An independently retained canonical call ending can remain.

Offsets use the recording clock only when known. Missing clocks read Time unavailable after timed items; they are not invented from wall time. A receipt outside the recording retains its offset and is labeled accordingly, without a seek control. No latency is calculated across different clocks.

GET /v1/calls/{call_id}/observability returns the same bounded closing_evidence: completeness, reason, capture_finished, counts, dropped, items, returned, limit, ending_reason, and final_state. Each item carries its stage, safe source/owner/reason, request/proposal identity, and nullable recording offset. The Calls aggregate interleaves those receipts with its existing transcript timeline. The paginated transcript endpoint keeps its existing turn/uncaptioned-speech stream; it does not acquire closing rows or a new cursor format. There is no separate closing endpoint or live stream.

These receipts describe existing voice closing decisions. They add no hangup policy, payment rule or setting, and do not give SMS a call-closing behavior.

What is recorded, and what is not

The rows are built from a fixed set of typed fields. Nothing the model or the caller wrote is kept with them. Skysay does not store, and cannot show, a tool's arguments, its result, the URL or headers it was called with, its response body, or any error message — redacted or otherwise. A tool row holds only:

  • the tool's kind (webhook, connection, send_sms, end_call, and so on),
  • an opaque internal reference to the configured tool,
  • whether it succeeded,
  • a closed error class if it failed, which is cancelled for a tool that was cancelled and unknown for every other failure,
  • its duration, which is empty when nothing measured it,
  • its offset into the call.

A language switch holds only the language moved from and to — checked against the languages frozen with the call, not against the agent's current settings — the reason (the agent followed the caller, or the switch was requested), and the offset. The utterance that triggered it is not recorded with the row; it is already an ordinary transcript turn.

The name on a tool row is looked up when the drawer opens, from the agent's current tool list. It is never copied into the stored row. Unlike the language set, which is frozen with the call, the agent's tools are not — which is why a tool the agent has since renamed or removed shows its kind instead of a name.

Each configured tool carries a stable internal identifier, and that is what the row records. The identifier survives any edit that keeps the tool's kind and name — you can change a webhook's URL, its description or a transfer destination and older calls keep showing that tool by name. Renaming or removing a tool retires its identifier, so rows recorded before the change show the tool's kind — "Webhook tool" — instead of a name. That is deliberate: showing a name the call never used, or a similar tool's name, would be worse than showing none.

The identifier appears as a read-only ref on each tool in the agent API. You cannot set it; a value sent in a request is ignored in favour of what is already stored.

Limits, and why a blank list is never "no tools ran"

At most 200 rows are recorded per call; past that the list is marked incomplete rather than silently trimmed, and the drawer says so.

Absence is always explained. A call handled before this existed, a call whose audio arrived through a self-hosted gateway, a call that failed before media started, and a call that ran a conversation flow each get a one-line reason instead of an empty list. GPT-Live calls record tool rows but not language switches, because that engine follows the caller's language on its own rather than through a controlled switch, and the drawer says that too. An empty list never means the agent made no tool calls — only an explicit "none recorded" does, and that appears only when the call was actually observed.

Ordering is by offset on the recording clock. A tool row marks when the call finished, not which turn caused it, so a row sits between turns rather than being attributed to one of them.

Deleting a recording, a retention expiry, or re-importing a corrected recording removes these rows along with it.

When the other party spoke but has no transcript

On a GPT-Live or Gemini Live call, the engine's own captions of the other party can be missing — most often for Sinhala. When that happens, the agent's replies are in the transcript but what the other party said is not.

So that such a call does not read as the agent talking to nobody, the transcript shows a "Spoke — no transcript" line where the other party's turn would be, under the same label as their turns (Caller, Called person on an outbound call, or the contact's name), with its offset into the call. It carries no words. Nothing the person said is known, only that they spoke.

The line is written only when all of these hold:

  • voice activity was detected on the line before the engine's next reply,
  • no caption came back for that speech, and
  • Skysay did not ask the engine for a line in between, such as a check-in, since the engine's reply could then be answering that request instead.

It is placed where that speech began. If the person said something that was captioned and then something that was not, only the uncaptioned part is marked.

Speech the engine never heard, such as someone who spoke only while the opening message was playing, is never marked, and neither is anything said once the call has started ending. Someone who starts over the end of the opening and keeps talking after it is marked where they began. A call on the standard pipeline never shows the line: its own transcriber writes the turn instead.

The rule is deliberately cautious. It can miss a moment -- for example when the engine did not answer the person and Skysay then asked whether they were still there -- rather than invent one. It can occasionally mark the agent's own voice coming back on a phone line as the other party, since both reach Skysay as sound on the line.

The line is evidence that the person spoke, not the other way round: a call without it does not mean the other party was silent. A call whose captions were all missing can have a transcript made only of the agent's turns. That transcript counts as available, like any transcript that has turns.

What reaches the API and post-call analysis

Tool rows and language switches are shown in the Calls workspace only. The public transcript API does not return them.

The "no transcript" moments are different, because they change what a call was. They are added to the transcript API beside the turns, never among them:

  • get_call_transcript keeps turns and text to captioned speech only, and adds an events list with one entry per moment: {"type": "caller_spoke_no_transcript", "speaker": "caller", "start_ms": ...}. There is no text, because none exists.
  • The transcript stream places the same entries between the turns, in reading order, under their own type, beside the existing turn entries. A client that reads stream entries should check type before reading turn fields such as data.text: a caller_spoke_no_transcript entry's data holds only speaker and start_ms.
  • Post-call extraction reads a [spoke, no transcript] line for the caller at that point, and is told the line carries no content. It can then see that the person answered without inventing what they said.

turns, text, the stream and the extraction input all read in the order the Calls workspace shows: by when each turn started, with the caller's own turns kept in the order they were spoken, and a moment before a turn that starts at the same instant. A caller turn whose start was measured on the recording after the call can therefore come before the agent reply it was answered with, so turns is not always in sequence order. Sort by sequence if you need the order the turns were stored in.

An integration that reads only turns sees no new entries. The call.completed, call.transcript.ready and call.recording.ready webhooks carry a pointer to the transcript rather than its turns, so they are unchanged. Turn counts, and campaign and call-group readiness, count transcript turns only.

Call variables on a call

For a call that ran with call variables, the selected call in Calls lists each variable the call carried — declared and built-in — with where its value came from (the API request, the campaign contact, a caller lookup, a test) and its state.

  • An ordinary variable shows its value.
  • A protected variable never shows its value, before or after the caller was confirmed. It reads Held until confirmed, or Released at turn N once the call's confirmation released it — together with how the caller was confirmed (their confirmation, or a matched value) and at which step.
  • The workflow's own checks are listed in order under Identity checks and SMS steps: each value match (Answer matched, Answer did not match or No usable answer) and each SMS the workflow sent, with its status (SMS sent, SMS failed, SMS delivery unknown, or SMS recorded, not sent in Talk and Chat). Neither shows what the caller said, the value compared, the SMS text or the recipient.

Where a call's workflow ended

A call that ran a conversation workflow says where the workflow ended in workflow, on GET /v1/calls/{call_id}, MCP get_call, and the call.completed and call.failed webhooks. The field is always present, and null for a call that ran no workflow (a Gemini Live call runs none). The GET /v1/calls list and the call's other reads do not include it.

"workflow": {
  "workflow_id": "wf_...",
  "version": 3,
  "status": "completed",
  "settled": true,
  "outcome": {"step_id": "cease_contact_stop", "type": "review_candidate", "label": "Asked not to be contacted — review"},
  "interrupts": ["cease_contact"]
}
FieldMeaning
workflow_id, versionThe workflow the call ran and its published version. version is null when the call ran an unpublished draft.
statusrunning, incomplete (the call ended before the workflow finished), completed, transferred or failed.
settledtrue once the workflow can no longer change. Read the call again while it is false.
outcomeThe last outcome step the workflow reached: its step_id and label as you wrote them, and its type (for example review_candidate, informational or terminal_note). It is set even when status is incomplete: the step was reached before the call ended. null when it reached none.
interruptsEvery global interrupt the workflow took, in the order first taken, each once: wrong_party, dispute, hardship, cease_contact, callback, human_request or language_change. Empty when it took none.

status and type are open: a later release can add a value, so handle one you don't recognise rather than rejecting it. label is for display only. Decide whether the workflow finished from status and settled, never from outcome being present. The object carries ids and your own labels only, never a value the call was given.

Act on interrupts, not on the step id or the label. Those are your own text and change when you edit the workflow; the interrupt names are fixed. The caller asked not to be contacted when interrupts contains "cease_contact", whatever the workflow did afterwards. Nothing adds the number to your do-not-call register by itself, so if you want to pause the number or send it for review, do it from this field.

The webhook carries the value at the moment the call ended. When its settled is false, for example when the call ended while the workflow was still running, read GET /v1/calls/{call_id} for the current value: the read is the authoritative record.

A campaign contact's outcome reports the same outcome as workflow_outcome and workflow_outcome_label, and the most recent interrupt as workflow_interrupt. For every interrupt the caller raised, read the call's interrupts.

outcome.label is the label the run was pinned with, with one exception: the old starter label that claimed the number would be added to the do-not-call register, however it is spaced or capitalised, reads as the current starter label, because nothing adds it. The workflow run record (GET /v1/agents/{agent_id}/workflow/runs/{run_id}, also shown in Talk) returns pinned_contract unchanged: it is the historical record, so a label there can differ from outcome.label.

Where a call's telemetry lives

A call's timing and cost evidence — the per-turn latency budget, the cost breakdown, and which figures were measured rather than estimated — is part of the call, so it is read on the call. Open a call in Calls and pick the Performance tab of the record that slides open beside the table.

The open record and the tab are both in the URL, so a performance view is a link you can send or bookmark:

https://skysay.ai/calls?call_id=cl_...&tab=performance

The former standalone /calls/performance page is gone. It now redirects to the Calls table, carrying a call_id through to the Performance tab of that call, so older links and bookmarks keep working. Nothing about the evidence itself changed, and the API is unaffected: the tab reads the same telemetry the page read.

Carrier-leg call quality

A recording is decoded after the browser-grade jitter buffer of the recorder, so it can sound clean while the phone on the other end heard something robotic. For a call carried by the Skysay SIP bridge, the call's evidence now also carries the bridge's own per-call counters for the leg between the bridge and the carrier, so that case can be told apart from a problem inside the platform without a packet capture.

Open the call in Calls and expand Technical details. Five rows sit beside RTP receive, each named by the direction the packets travelled:

  • Carrier → SIP bridge — the negotiated codec, the direction, whether the bridge's jitter buffer was on, and what the bridge received from the carrier: packets, sequence gaps, late and delayed packets, jitter-buffer loss and the maximum per-packet latency.
  • SIP bridge → carrier — the audio frames the bridge encoded and sent toward the carrier, and its maximum output latency.
  • Room → SIP bridge — the AI agent's audio as the bridge received it from the media room: packets, gaps, late and delayed packets (with the total delayed time), jitter-buffer loss and reconnects. Delayed intervals between the agent's utterances are normal; delayed or late packets during an utterance are not.
  • SIP mixer — whether the bridge paced its output on time: mixes, on-time mixes, timing jumps and dropped input frames.
  • Carrier reception (RTCP) — the carrier's own reception report, kept as a separate row on purpose: the four rows above are measured at the bridge and cannot show what the carrier received. This row reads Not captured today (the SIP bridge does not read RTCP). Even when captured, a receiver report describes loss and jitter at the carrier's receiver only; it cannot clear the path beyond the carrier to the handset or detect every audible distortion.

GET /v1/calls/{call_id}/observability returns the same projection as dashboard_summary.media_evidence.sip_call_statistics, with the sections carrier_to_bridge, bridge_to_carrier, room_to_bridge and mixer, plus direction, audio_codec, jitter_buffer_enabled, logged_at and the platform's own livekit_sip_call_id (an opaque reference Skysay support can correlate with the carrier leg). Its status is complete, partial (a section is missing and reason names it) or unavailable with a reason such as not_captured for a call recorded before this evidence existed. The carrier's report is the separate media_evidence.carrier_reception group (status, reason, source: carrier_rtcp), unavailable / rtcp_not_captured today. A counter the bridge did not report is absent rather than zero, and a group the capture never saw reads as not captured, never as a clean leg or zero loss.

Inbound attempts rejected before connection

An incoming call can reach Skysay but be rejected before the AI agent answers. When the called number identifies your workspace, the rejected attempt appears in Calls with its time, direction and a customer-readable reason. Repeated delivery of the same carrier call does not create another attempt.

For example, missing pricing for a managed number is a configuration problem, not evidence that the caller hung up or that the destination was busy. Follow the explanation on the call or the number's Activation checks. An attempt rejected before billable media starts has no conversation recording or transcript to play; do not interpret missing audio as a recording failure.

The same applies when the number's AI agent has a published voice setup the voice agent could not run: no instructions, no exact voice, a component that cannot run the call's language, or a voice setup saved before a platform update that it no longer accepts. The call is rejected before the caller is connected and nothing is reserved, instead of being answered by a line with no agent on it. The agent shows the same reason in the workspace, with the fix; for an out-of-date setup that is saving the agent's voice setup again. Outbound calls to such an agent are refused the same way; see Customer applications.

Only attempts attributable to your workspace appear in its history. Calls to unknown numbers are not assigned to a customer workspace.