Appearance
Recording transcripts, audio redaction and the screen-monitoring notice
Spanish version: Transcripciones, redacción y grabación de pantalla.
This guide covers two things the platform does with a recorded call and the routes that expose them: the post-call transcript and the redaction of personal data in the audio. Further sections give the contract of the screen-monitoring notice routes and of the screen recording and live screen-monitor session routes. Field names, enums and status codes are copied from the vendored OpenAPI document (core v0.105.0); the API Reference generated from it is the authority on shapes.
Availability
| Capability | Availability |
|---|---|
GET /v1/recordings/{id}/transcript | Published in core v0.103.0 (core#1971, core#2064) |
GET / POST /v1/recordings/{id}/redaction, POST /v1/recordings/{id}/original-url | Published in core v0.103.0 (core#2016) |
GET /v1/screen-monitor/notice, POST / GET /v1/screen-monitor/acks | Published in core v0.103.0 (core#1989) |
PUT /v1/screen-recordings/{id}/chunks/{n}, POST /v1/screen-recordings/gaps | First in core v0.104.0 (core#2036); present in the v0.105.0 spec |
PUT /v1/screen-monitor/armed, POST /v1/screen-monitor/sessions, POST .../sessions/{id}/signal, POST .../sessions/{id}/end | First in core v0.104.0 (core#1990); present in the v0.105.0 spec |
GET /v1/screen-monitor/sessions | First in core v0.105.0 (core#1990) |
Every screen-monitor and screen-recordings route answers 404 not_found while the tenant's screen_monitor_enabled flag is off, except the ledger reads (GET /v1/screen-monitor/acks, GET /v1/screen-monitor/sessions) and POST .../sessions/{id}/end, which the spec states do not consult it.
Whether a given recording has a transcript depends on eligibility (see below) and on the cell having a transcription engine configured. A recording without one is 404, not an empty document.
Transcript
http
GET /v1/recordings/{id}/transcriptThe 200 body is { "transcript": { ... } } with the fields of the RecordingTranscript schema:
| Field | Type | Meaning |
|---|---|---|
recording_id | uuid | The recording. |
provider | string | Speech-to-text provider that produced it. |
model | string | Model that produced it. |
language | string or null | Language the provider reported, when it reported one. |
audio_seconds | integer | Seconds of audio billed. |
segment_count | integer | Number of timed segments. |
created_at | date-time | When the transcript was stored. |
segments | array | Timed segments, ordered by seq. |
Each entry of segments has seq (integer from 0), channel (audio channel, 0 or 1), start_ms, end_ms and text.
Who can read it
A transcript is the raw personal data of a call, so the route is narrow:
- Allowed: a dashboard login (
ddu_) with a supervisor or admin seat, or anaccount_adminlogin without a seat. - Not allowed: a
dd_tenant API key, a widget token and an agent seat all get403 role_forbidden.
What is audited
Every delivery appends one read event in the same transaction as the read: who read it, when, which recording and how many segments. The event never holds the text. If the event cannot be written, no text is returned and the answer is 503; retry later.
When it is 404
404 is one answer for several situations, so the route cannot be used to probe other tenants: the recording belongs to another tenant, its audio was purged (the purge deletes the transcript with it), it was never eligible for a transcript, or the id is malformed. A recording under legal_hold keeps its transcript.
Status codes
| Code | Meaning |
|---|---|
200 | The transcript. |
401 | Missing or invalid credentials. |
403 | The caller's role cannot read transcripts. |
404 | No transcript for this recording in this tenant. |
503 | The read event could not be recorded, so no text was returned. |
If the transcription provider is not available
Transcription runs after the call, outside the request path, and it fails closed:
- Paid transcription spend is capped per month by the platform's spend brake. A paid provider is only used when the cap is positive; with a cap of 0 the job stops without spending and does not fall back to a local engine silently.
- If the transcription engine refuses the requested provider, the job ends without storing anything and the reserved spend is released. If it answers with a different provider than the one reserved, core does not store that answer either.
- Eligibility is decided before any spend: the plan tier must include it (Scale and Enterprise), the recording must be available, within the duration limits, answered by a person and not an AI call, and without a previous transcript. What does not qualify is not reserved, not billed and not queued again.
The visible effect for an API client is the same in every case: the recording has no transcript and GET .../transcript answers 404. There is no "pending" state in the response.
Audio redaction
Redaction never edits the stored recording. The platform keeps the original untouched, with its retention and legal hold, and builds a derived copy with the sensitive spans silenced or replaced by a tone. That copy is the default view.
What gets redacted
A span is a time range plus a category from a closed list. The request carries no value, and the stored redaction holds none either:
| Field | Type | Notes |
|---|---|---|
start_ms | integer, min 0 | Start of the span. |
end_ms | integer | End of the span. |
category | enum | card, ssn, bank_account, pin. |
Only WAV recordings can be redacted. Any other format is 422 unsupported_codec.
Ask for a redaction
http
POST /v1/recordings/{id}/redactionjson
{
"method": "silence",
"segments": [
{ "start_ms": 12400, "end_ms": 15800, "category": "card" }
]
}segmentsis required: 1 to 500 entries.methodissilence(default) orbeep.- The spans are unioned with those already stored, and the copy is rebuilt from the original. The call is idempotent;
versiongoes up only when something new was added. - The caller needs the same role as for the original (see below).
The 200 body is { "redaction": { ... } } with recording_id, method, segments, redacted_ms, version, codec, requested_by, created_at and updated_at. Status codes: 400, 401, 403, 404, 409, 422.
Read the redaction
http
GET /v1/recordings/{id}/redactionReturns the same redaction object: times, categories, method and version, never the values and never object-store keys. 404 when the recording has no redaction. Status codes: 200, 401, 403, 404.
Playback and the original
POST /v1/recordings/{id}/playback-url returns the redacted copy when the recording has one. If the redaction row exists but its copy is gone (purged), the route does not fall back to the original.
The unredacted audio has its own door:
http
POST /v1/recordings/{id}/original-urljson
{ "reason": "Dispute 2026-0417: customer denies the payment promise" }- Who can ask: a
dd_tenant key or a dashboard login with theaccount_adminrole. Anything else is403 role_forbidden. reasonis required, non-empty, at most 500 characters.- The actor and the reason are written to an append-only access log before the URL is returned; if the log entry cannot be written, no URL is issued.
- The
200body is a signed, short-lived URL (url,expires_at). Status codes:400,401,403,404,409.
Screen-monitoring notice and acknowledgements
Before a seat's screen may be observed, the seat acknowledges a versioned notice. This section is the contract of the routes only. The legal text of the notice is not published in this guide; see Not covered here.
Read the current notice
http
GET /v1/screen-monitor/noticeThe 200 body is { "notice": { ... } } with version, lang, title, body, text_sha256 (SHA-256 hex of body in UTF-8), effective_at and published_at. The notice is a global, append-only catalogue: publishing a newer version invalidates every acknowledgement of the older ones without touching them.
- Reachable by a
dd_key and by addu_login; addw_widget token is403. 404 not_foundwhen the tenant'sscreen_monitor_enabledflag is off, and also when no version has been published yet.- Status codes:
200,401,403,404.
Acknowledge it from one's own seat
http
POST /v1/screen-monitor/acksjson
{ "notice_version": 3, "scope": "live_monitoring" }| Field | Type | Notes |
|---|---|---|
notice_version | integer, min 1 | Required. Must be the current version. |
scope | enum | Required: live_monitoring or screen_recording. |
agent_id | uuid | Optional. If present it must be the caller's own seat. |
- The two scopes are acknowledged separately: accepting live monitoring does not authorize recording, and a new notice version invalidates both.
- Only a
ddu_login with a linked seat can acknowledge. Add_key is403 forbidden; addu_with no active seat is403 seat_not_linked; anagent_idthat is not the caller's own seat is403 forbidden. - The seat comes from the bearer, never from the body.
- Repeating the same version and scope returns the first acknowledgement (
200, no new row). The ledger is append-only. 404 not_foundwhilescreen_monitor_enabledis off. With the flag on:409 no_notice_publishedwhen no version exists,409 stale_noticewhennotice_versionis not the current one.- The
200body is{ "ack": { ... } }withid,agent_id,notice_version,scope,text_sha256(copied from the notice the seat saw),channel(dashboard_checkin) andacked_at. Status codes:200,400,401,403,404,409.
List acknowledgements
http
GET /v1/screen-monitor/acks?limit=50&cursor=...&agent_id=...Newest first, paginated by cursor: limit 1 to 200 (default 50; a larger value is capped, not refused), cursor is the next_cursor of the previous page and agent_id filters to one seat. The body is { "acks": [...], "next_cursor": ... } and next_cursor is null on the last page.
Allowed only to a seat whose template role is supervisor or admin (403 role_forbidden otherwise; a dd_ key and a ddu_ with no seat are 403). It does not depend on the screen_monitor_enabled flag: it is the record of what was accepted, and turning the function off neither deletes nor hides it. Nothing crosses tenants. Status codes: 200, 400, 401, 403.
Screen recording
The agent's browser captures its own screen and uploads it to core in chunks. All of this is behind the tenant flag screen_monitor_enabled (see the note under Availability) and behind a separate acknowledgement.
Consent: the screen_recording acknowledgement
Recording needs a current acknowledgement with scope: "screen_recording" (see Acknowledge it from one's own seat). It is separate from live_monitoring: accepting one never authorizes the other, and a new notice version invalidates both. GET /v1/me reports it per scope in screen_monitor.scopes.<scope>.ack_current (ScreenMonitorStatus); a scope is authorized only when enabled is true and ack_current is true.
Upload a chunk
http
PUT /v1/screen-recordings/{id}/chunks/{n}?call_uuid=...
Content-Type: video/webmidis a UUID the client chooses, one per call, so a retry is idempotent.nis the chunk number, from 0 to 1,000,000.call_uuidis required (1 to 128 characters) and must be a call of the caller's own seat.- The body is
video/webmorvideo/mp4, at most 4,000,000 bytes per chunk (413above that,415for another type). - Only a
ddu_login with a linked seat may upload; add_key, addu_with no seat and addw_token are403. - The seat, the call and the retention come from core, not from the client. The chunk is sealed with the tenant's key (AES-256-GCM) before it reaches the store, bound to
idandn. - Without a current
screen_recordingacknowledgement the answer is403 screen_recording_not_acknowledged, nothing is stored and anot_armedgap is recorded.
201 means the chunk was sealed and stored; 200 means the same bytes were already stored (status: duplicate). The body is { "chunk": { ... } } with recording_id, seq, bytes (size before sealing), status (stored or duplicate) and retained_until (a date, inherited from the voice recording of the same call).
| Code | Meaning |
|---|---|
404 | Flag off, or call_uuid unknown or another agent's call (same answer for both). |
409 chunk_conflict | That n already holds other bytes. |
409 recording_conflict | The call already has a recording under another id, or id belongs to another call. |
410 | The screen recording was purged at the end of its retention. |
503 | The store did not take the chunk; retry it. An upload_failed gap is recorded. |
Other codes: 400, 401, 403, 413, 415.
Declare a gap
http
POST /v1/screen-recordings/gapsjson
{ "call_uuid": "...", "reason": "agent_stopped", "note": "capture ended by the agent" }reason is not_armed, agent_stopped or upload_failed; note is optional, at most 500 characters. It is idempotent per call, reason and source, needs no acknowledgement (a gap is evidence, not content) and applies the same seat and call rules as the upload. The 200 body is { "gap": { "call_uuid", "reason", "source" } } with source always agent for this route; core writes its own not_armed gap when a chunk arrives without an acknowledgement.
Retention, reads and purge
retained_until in the chunk response is inherited from the voice recording of the same call, and an upload to a recording purged at the end of its retention is 410. The vendored v0.105.0 spec does not publish a route to read a screen recording back, a signed URL for it, or a purge route, so this guide does not describe them.
Live screen-monitor sessions
A supervisor can watch an agent's screen live. Core does not see the video: it validates, writes a ledger row and signals both browsers, which negotiate the media peer to peer.
- Arm.
PUT /v1/screen-monitor/armedwith{ "armed": true, "tab_id": "..." }(tab_id1 to 64 characters) from the agent's own seat once its dashboard holds a capture. Arming needs the seat to be checked in (409 not_checked_in) and to hold a currentlive_monitoringacknowledgement (409 no_ack). One tab per seat: arming with a differenttab_iddisplaces the previous one. The armed state lives in the process, so a restart disarms everyone. - Open.
POST /v1/screen-monitor/sessionswith{ "agent_id": "..." }from asupervisororadminseat.201returns{ "session": { ... } }withresult: "started". Every outcome writes a ledger row, refused ones included.409codes:no_ack,not_armed,agent_busy,agent_unreachable. An unknown agent, an agent of another tenant and an inactive agent are the same404. - Signal.
POST /v1/screen-monitor/sessions/{id}/signalwithkind(offer,answerorcandidate) andpayload.offeronly from the agent,answeronly from the supervisor. The payload is forwarded and never stored; at most 16,384 bytes (413 payload_too_large) and 60 messages per 10 seconds per session (429 rate_limitedwithRetry-After).202returnsdelivered,falsewhen the other party had no live socket. A closed session is409 session_closed. - End.
POST /v1/screen-monitor/sessions/{id}/end, optional body{ "reason": "peer_failed" }. Either party may call it, it is idempotent, and it works with the flag off. The system also closes a live session on checkout, lost socket, lapsed credential, or when the flag, the supervisor's role or the agent's acknowledgement stop holding.
GET /v1/screen-monitor/sessions lists the ledger (supervisor or admin seat, newest first, limit 1 to 200 default 50, cursor, agent_id, and result in started, ended, failed, refused). A row (ScreenMonitorSession) has id, supervisor_user_id, supervisor_id, agent_id, notice_version, result, reason (flag_off, role, agent_not_found, no_ack, not_armed, agent_busy, agent_stopped, credential_lapsed, checkout, peer_failed or null), started_at and ended_at. It never carries signalling payload and does not depend on the flag.
Who can change the compliance configuration
Writing the tenant's compliance configuration takes an administrator: a dd_ key or an account_admin login (core#2438). A member login gets 403 role_forbidden on every write. In this guide's neighbourhood that is:
PUT /v1/recordings/{id}/legal-hold, to place or lift a legal hold.PUT /v1/recording-policy, the tenant recording policy.
Reading does not change: GET /v1/recording-policy and the recording's own metadata, where the hold shows, stay open to the same readers as before. The same rule covers the artificial-voice disclosure, the retry rules and the AI pause and resume; those routes are described in the generated API reference, not in this guide.
Not covered here
Redaction of personal data in the screen recording (core#2017) is not published in the v0.105.0 spec, so this guide makes no claim about it.
The legal text of the monitoring notice and the jurisdiction-by-jurisdiction legal matrix are not published in this guide. They wait on the decision tracked in macro-tasks#415, and the Roadmap lists them as not served. The routes above only carry whatever notice text the platform publishes; they do not make a claim about what the law requires.
How spans are detected is not part of the redaction routes: the endpoint receives spans, it does not find them. Redaction of Opus audio is not served; only WAV is, as the 422 above states.