Appearance
Grabaciones
Generado desde
DialerDigital/corecommit0fc18008b3c8— especificaciónopenapi/dialer-v1.yamlv0.134.0. No se edita a mano; consulta Cómo se construye esta referencia. Descargar la especificación OpenAPI.
Traducción en curso
Esta referencia se genera desde la especificación OpenAPI. Las cadenas que todavía no tienen traducción aparecen en inglés.
Evidencia de grabación de llamadas: metadatos con retención bloqueada, reproducción firmada, retención legal, exportación masiva, política por tenant.
Endpoints
Subir un fragmento de la grabación de pantalla del puesto PROPIO
http
PUT /v1/screen-recordings/{id}/chunks/{n}Almacena el fragmento n (desde 0) de la grabación de pantalla id de una llamada. El MediaRecorder del navegador envía los fragmentos a medida que se producen; id es un UUID que el cliente elige para toda la grabación, de modo que un reintento es idempotente.
Todo lo que identifica la grabación proviene del core, no del cliente: el puesto, del bearer; la llamada, de call_uuid (que debe ser una llamada de ESTE puesto) y la retención, de la grabación de voz de la misma llamada (retained_until). Los instantes de inicio y fin son los de la llamada y no se vuelven a almacenar.
El fragmento se sella con la clave del tenant (AES-256-GCM) antes de llegar al almacén, vinculado a id y n: el objeto no puede abrirse en otro lugar.
Compuertas, en orden: solo un inicio de sesión ddu_ con un puesto vinculado (403 para una clave dd_, un ddu_ sin puesto y un token ddw_); 404 not_found mientras el indicador screen_monitor_enabled del tenant esté desactivado; 404 not_found para un call_uuid desconocido o que es de otro agente (la misma respuesta, y una fila de auditoría registra cuál); 403 screen_recording_not_acknowledged cuando el puesto no tiene un acuse screen_recording vigente (no se almacena nada y se registra un hueco not_armed).
Repetir un fragmento con los mismos bytes responde 200 duplicate; otros bytes bajo el mismo n son 409 chunk_conflict; un segundo id de grabación para la misma llamada es 409 recording_conflict. Un fragmento que el almacén no aceptó es 503 y registra un hueco upload_failed.
Límites: 4 000 000 de bytes por fragmento, n de 0 a 1 000 000.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string (uuid) | sí | Id de la grabación, elegido por el cliente, uno por llamada. |
n | path | integer | sí | Número de fragmento, desde 0. |
call_uuid | query | string | sí | La llamada que se graba; debe ser del propio solicitante. |
Respuestas
200
El fragmento ya estaba almacenado con los mismos bytes.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
chunk | object | sí | |
chunk.recording_id | string (uuid) | sí | |
chunk.seq | integer | sí | |
chunk.bytes | integer | sí | Tamaño del fragmento antes de sellarlo. |
chunk.status | stored · duplicate | sí | |
chunk.retained_until | string (date) | sí | Heredado de la grabación de voz de la misma llamada. |
201
El fragmento se selló y se almacenó.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
chunk | object | sí | |
chunk.recording_id | string (uuid) | sí | |
chunk.seq | integer | sí | |
chunk.bytes | integer | sí | Tamaño del fragmento antes de sellarlo. |
chunk.status | stored · duplicate | sí | |
chunk.retained_until | string (date) | sí | Heredado de la grabación de voz de la misma llamada. |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409
chunk_conflict: ese n ya contiene otros bytes. recording_conflict: la llamada ya tiene una grabación con otro id, o id pertenece a otra llamada.
json
{
"error": {
"code": "chunk_conflict",
"message": "that chunk number already holds other bytes"
}
}410
La grabación de pantalla se purgó al terminar su retención.
413
El fragmento supera los 4 000 000 de bytes.
415
El cuerpo no es video/webm ni video/mp4.
503
El almacén no aceptó el fragmento; reinténtalo.
Declarar que falta la pantalla de una llamada, y por qué
http
POST /v1/screen-recordings/gapsEl cliente del agente informa de un hueco que conoce: agent_stopped (el agente terminó la captura), upload_failed (los fragmentos no llegaron) o not_armed (la captura nunca se inició). El core escribe su propio hueco not_armed cuando llega un fragmento sin un acuse vigente.
Idempotente: una fila por llamada, motivo y origen. El libro es append-only. Se aplican las mismas reglas de puesto y llamada que en la subida de fragmentos (404 not_found para una llamada que no es del solicitante; 404 mientras screen_monitor_enabled esté desactivado). No se requiere acuse: un hueco es evidencia, no contenido.
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
call_uuid | string | sí | |
reason | not_armed · agent_stopped · upload_failed | sí | |
note | string | no |
Respuestas
200
El hueco queda registrado (el primero cuando se repite).
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
gap | object | sí | |
gap.call_uuid | string | sí | |
gap.reason | not_armed · agent_stopped · upload_failed | sí | |
gap.source | agent | sí |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}Declarar qué tramos de la grabación de pantalla del puesto PROPIO mostraron PII
http
POST /v1/screen-recordings/{id}/pii-marksEl dashboard del agente declara los intervalos durante los cuales la pantalla mostró una superficie que él pinta como PII, y hasta dónde ha clasificado el video (covered_through_ms). El core los guarda en modo append-only junto a la grabación; la copia redactada que los supervisores ven por defecto se construye a partir de ellos (core#2017, ADR #266): cada marca, ampliada con un margen a ambos lados, y todo lo posterior a covered_through_ms se ennegrece. Una grabación sin fila de cobertura se ennegrece por completo, de modo que un cliente que nunca informa deja una copia negra, nunca una a la vista.
zone es un catálogo CERRADO: consumer_card (el registro del consumidor), full_number (el número de teléfono sin enmascarar), payment (tarjeta, cuenta bancaria, SSN, PIN) y focus_lost (el dashboard no tiene el foco o no es visible, por lo que no sabe qué muestra la pantalla). Una zona fuera del catálogo es 422 invalid_zone; nunca se almacena como texto libre. Una marca contiene solo instantes, nunca texto ni valores.
Los instantes son milisegundos desde el inicio del video de la grabación, 0 <= start_ms < end_ms <= 86 400 000. Una solicitud admite como máximo 200 marcas y una grabación contiene como máximo 1 000 (422 too_many_marks, 422 mark_limit_reached; una solicitud rechazada no almacena nada). marks puede estar vacío: una solicitud con solo covered_through_ms es un latido (heartbeat).
Idempotente: una marca ya registrada (mismos start_ms, end_ms, zone) se cuenta en duplicates y no se almacena dos veces. covered_through_ms solo avanza: un valor menor que el registrado se ignora, no es un error (es un reintento que llegó tarde), y la respuesta indica el valor vigente.
Mismas compuertas que la subida de fragmentos, en orden: solo un inicio de sesión ddu_ con un puesto vinculado (403 para una clave dd_, un ddu_ sin puesto y un token ddw_); 404 not_found mientras el indicador screen_monitor_enabled del tenant esté desactivado; 404 not_found para una grabación desconocida o que pertenece a otro puesto o tenant (una sola respuesta para los tres casos; la grabación existe una vez que llegó su primer fragmento, así que un cliente que va por delante de su subida recibe 404 y reintenta); 403 screen_recording_not_acknowledged cuando el puesto no tiene un acuse screen_recording vigente; 410 una vez que la grabación fue purgada.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string (uuid) | sí | El id de la grabación que el cliente eligió para la subida de fragmentos. |
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
marks | arreglo de object | no | |
marks[].start_ms | integer | sí | |
marks[].end_ms | integer | sí | |
marks[].zone | consumer_card · full_number · payment · focus_lost | sí | |
covered_through_ms | integer | sí |
Respuestas
200
Las marcas están registradas y la cobertura está vigente.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
pii_marks | object | sí | |
pii_marks.recording_id | string (uuid) | sí | |
pii_marks.stored | integer | sí | Marcas escritas por esta solicitud. |
pii_marks.duplicates | integer | sí | Marcas ya registradas. |
pii_marks.covered_through_ms | integer | sí | El valor vigente, el mayor entre el registrado y el enviado. |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}410
La grabación de pantalla se purgó al terminar su retención.
422
invalid_zone: la zona no está en el catálogo. invalid_interval: una marca fuera de 0 <= start_ms < end_ms <= 86 400 000. too_many_marks: más de 200 en una solicitud. mark_limit_reached: la grabación contendría más de 1 000.
json
{
"error": {
"code": "invalid_zone",
"message": "zone must be one of: consumer_card, full_number, payment, focus_lost"
}
}Solicitar ver la grabación de pantalla REDACTADA de una llamada; devuelve una URL firmada de corta duración
http
POST /v1/screen-recordings/playbackRequiere un puesto de supervisor o administrador (403 role_forbidden en otro caso; una clave dd_ no tiene puesto, por lo que 403 forbidden). NO consulta screen_monitor_enabled: desactivar la función no oculta lo que ya se grabó.
Lo que se sirve es la copia REDACTADA: el video con cada tramo que el dashboard marcó como PII (y cada tramo que nunca clasificó) ennegrecido (core#2124, ADR #266). Se sirve solo cuando la copia está ready. Mientras se construye, o se reconstruye tras la llegada de una marca más reciente, la respuesta es 409 redaction_pending; si no pudo construirse, 409 redaction_failed. El original NUNCA se sirve en su lugar: el video sin enmascarar es una ruta aparte, POST /v1/screen-recordings/original-playback.
La fila de acceso (quién, qué call_uuid, qué variant, hasta cuándo) se escribe en la misma transacción ANTES de emitir la URL; sin fila no hay URL. Los fragmentos se almacenan como texto cifrado, así que la URL no es una URL del almacén: apunta a GET /v1/screen-media/{token}, que NO necesita bearer (el token firmado de la ruta es la credencial, válido hasta expires_at, 5 minutos) y por tanto puede ser el src de un <video>. Esa ruta transmite los fragmentos descifrados en orden con el tipo de contenido almacenado y Cache-Control: no-store; un token falsificado, expirado o sin fila de acceso que lo respalde es 404, una grabación purgada desde entonces es 410, y un token emitido para la copia redactada cuya copia dejó de estar disponible (se reconstruye por una marca más reciente) es 409, nunca el original. No forma parte de la superficie pública del widget.
gaps lista por qué la pantalla puede estar incompleta (not_armed, agent_stopped, upload_failed). Una llamada sin grabación es 404 (aquí no se devuelven los huecos de una llamada que nunca recibió un fragmento); una purgada es 410 y no escribe fila de acceso.
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
call_uuid | string | sí |
Respuestas
200
El acceso queda registrado y la URL está lista.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
playback | object | sí | |
playback.recording_id | string (uuid) | sí | |
playback.call_uuid | string | sí | |
playback.variant | redacted | sí | Qué video sirve la URL. |
playback.url | string | sí | Ruta bajo el origen de la API: /v1/screen-media/{token}. |
playback.expires_at | string (date-time) | sí | |
playback.retained_until | string (date) | sí | |
playback.chunks | integer | sí | |
playback.content_type | video/webm · video/mp4 | sí | |
playback.gaps | arreglo de object | sí | |
playback.gaps[].reason | not_armed · agent_stopped · upload_failed | sí | |
playback.gaps[].source | server · agent | sí | |
playback.gaps[].note | string,null | sí |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409
redaction_pending: la copia redactada aún no está lista (o se está reconstruyendo). redaction_failed: no pudo construirse. No se escribe fila de acceso ni se emite URL; no se sirve el original en su lugar.
json
{
"error": {
"code": "redaction_pending",
"message": "the redacted copy is not ready yet; the original is never served in its place"
}
}410
La grabación fue purgada.
Solicitar ver la grabación de pantalla SIN REDACTAR de una llamada, con un motivo; devuelve una URL firmada de corta duración
http
POST /v1/screen-recordings/original-playbackEl video sin enmascarar, que puede mostrar la tarjeta o el número completo de un consumidor (core#2124, ADR #266). Requiere un inicio de sesión del dashboard (ddu_) con el rol account_admin y nada más: un member, un supervisor sin ese rol, una clave dd_ de API y cualquier otro principal reciben 403 role_forbidden. No necesita puesto de agente y, como la ruta redactada, no consulta screen_monitor_enabled.
reason es obligatorio (una cadena no vacía de como máximo 500 caracteres, en otro caso 400). La fila de acceso (el actor user:<id>, el reason, variant: original, el call_uuid y cuándo expira) se escribe en la misma transacción ANTES de emitir la URL; sin fila no hay URL. La URL es del mismo tipo que la redactada (GET /v1/screen-media/{token}, sin bearer, 5 minutos, Cache-Control: no-store), pero la fila de acceso a la que apunta dice original, por lo que transmite los fragmentos originales. Una llamada sin grabación es 404; una purgada es 410 y no escribe fila de acceso. La contraparte de audio es POST /v1/recordings/{id}/original-url.
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
call_uuid | string | sí | |
reason | string | sí | Por qué se necesita el original; se almacena junto con el acceso. |
Respuestas
200
El acceso queda registrado y la URL está lista.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
playback | object | sí | |
playback.recording_id | string (uuid) | sí | |
playback.call_uuid | string | sí | |
playback.variant | original | sí | |
playback.url | string | sí | Ruta bajo el origen de la API: /v1/screen-media/{token}. |
playback.expires_at | string (date-time) | sí | |
playback.retained_until | string (date) | sí | |
playback.chunks | integer | sí | |
playback.content_type | video/webm · video/mp4 | sí | |
playback.gaps | arreglo de object | sí | |
playback.gaps[].reason | not_armed · agent_stopped · upload_failed | sí | |
playback.gaps[].source | server · agent | sí | |
playback.gaps[].note | string,null | sí |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}410
La grabación fue purgada.
Listar grabaciones de llamadas (filtradas, paginadas por keyset)
http
GET /v1/recordingsMetadatos de grabación de las llamadas del tenant, las más recientes primero. El AUDIO nunca se sirve aquí: obtén una URL firmada de corta duración mediante POST /v1/recordings/{id}/playback-url. q es una búsqueda de texto libre sobre el número de teléfono enmascarado, account_ref y cualquier referencia externa. from/to acotan started_at. Los ids de filtro ajenos o mal formados son indistinguibles de «sin coincidencias» (RLS), nunca una fuga.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
from | query | string (date-time) | no | Límite inferior RFC 3339 (campo específico del endpoint; por defecto los últimos 30 días UTC donde se indica). |
to | query | string (date-time) | no | Límite superior RFC 3339. |
agent_id | query | string (uuid) | no | |
campaign_id | query | string (uuid) | no | |
disposition | query | string | no | Coincidencia exacta contra un vocabulario ABIERTO: call_attempts.disposition se escribe TEXTUALMENTE (20260725000003_create_disposition_engine.exs) y los valores del motor — answered_human, abandoned, delivered, ai_conversation, answered_machine, ai_stream_lost, busy, no_answer, canceled, rejected, failed — son un SUBCONJUNTO NOMBRADO de él, no su frontera: los resultados tipificados del agente o de la IA (promise_to_pay, wrong_number) y los marcadores de plataforma (not_placed) son igualmente válidos. Un código desconocido responde 200 con cero filas, nunca 400. Lo que SÍ es 400 es una FORMA mal formada: la cadena vacía, y el parámetro repetido o enviado en forma de arreglo. |
q | query | string | no | Búsqueda de texto libre sobre el número de teléfono enmascarado, account_ref y cualquier referencia externa. |
limit | query | integer | no | Tamaño de página (por defecto 100, máximo 1000). |
cursor | query | string | no | Cursor keyset opaco del next_cursor de la página anterior. |
Respuestas
200
Una página de grabaciones.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
recordings | arreglo de RecordingListItem | sí | |
recordings[].id | string (uuid) | sí | |
recordings[].call_id | string | sí | El uuid de FreeSWITCH de la llamada (= call_attempts.call_uuid). |
recordings[].started_at | string (date-time) | sí | UTC. |
recordings[].local_tz | string | null | no | La zona IANA del destinatario (p. ej. America/Chicago); la interfaz muestra la hora local a partir de ella. |
recordings[].agent | object | null | no | Desnormalizado para mostrar; null para llamadas de IA o entrantes sin atender. |
recordings[].account_ref | string | null | no | Referencia externa de la deuda / número de cuenta. |
recordings[].phone_number | string | sí | El E.164 de la otra parte. |
recordings[].duration_secs | integer | null | no | |
recordings[].disposition | string | null | no | La tipificación de la llamada grabada, DERIVADA de su intento CDR terminal (la última corrección compensatoria, o el original cuando no llegó ninguna). null cuando la llamada no tiene fila CDR resoluble. |
recordings[].two_party_consent_state | string | null | no | Se establece cuando el estado del destinatario exige el consentimiento de todas las partes; el valor es el código de estado para el que se activó el indicador (p. ej. WA). null = no aplica. |
recordings[].attest_a | boolean | no | La pierna saliente llevó atestación STIR/SHAKEN A. |
recordings[].ai_used | boolean | sí | |
recordings[].retained_until | string (date) | no | Fecha mínima de retención, congelada en la creación (doctrina de instantánea). |
recordings[].legal_hold | boolean | sí | |
recordings[].status | pending · available · purged · lost | no | Dónde está el audio. pending = la llamada está grabada y el audio aún no se ha subido; available = subido y reproducible; purged = eliminado al terminar su retención (la fila sobrevive como evidencia); lost = la llamada terminó, nunca se confirmó una subida y el almacén de objetos no contiene audio (por ejemplo, el pod de medios murió antes de que el sidecar lo subiera). Una fila lost no tiene reproducción (409) y lleva lost_at y lost_reason. |
recordings[].lost_at | string (date-time) | null | no | Cuándo se cerró la fila como lost. Se conserva si el audio llegó tarde y la fila volvió a available. |
recordings[].lost_reason | no_upload_after_call_end · no_upload_call_never_ended | null | no | Por qué se cerró como lost; null cuando nunca se cerró así. |
recordings[].composed_from_parts | boolean | no | true cuando el audio se compuso a partir de los fragmentos que el pod de medios subió MIENTRAS duraba la llamada, porque el archivo completo nunca llegó (por ejemplo, el pod murió a mitad de llamada). La fila está available y es reproducible, pero el audio está TRUNCADO: consulta audio_gap_ms. false para una grabación cuyo archivo llegó completo. |
recordings[].audio_gap_ms | integer (int64) | null | no | El hueco declarado de una grabación composed_from_parts, en milisegundos: la duración de la llamada según el CDR menos el audio realmente compuesto (incluye el segundo o dos que tarda el servidor de medios en empezar a escribir). null cuando la grabación no se compuso a partir de fragmentos, o la llamada nunca registró un fin y no se puede calcular el hueco. |
next_cursor | string | null | sí | Cursor keyset opaco; null en la última página. |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Exportar los datos que se conservan sobre una persona
http
POST /v1/privacy/subject-exportsReúne, para UNA persona indicada por phone (E.164) o consumer_ref (exactamente uno), lo que la plataforma conserva de ella en este tenant, y lo escribe como un ZIP en el bucket de exportaciones (expira según su propio calendario): un JSON por categoría (debts, contacts, call_attempts, consents, sms_messages, email_messages, ai_conversations, recordings, transcripts) y un manifest.json que nombra las tablas leídas, las filas por categoría, las categorías cortadas por el tope de filas y lo que se omite y por qué (el audio de las grabaciones nunca se empaqueta). La respuesta lleva el mismo manifiesto y una URL firmada de corta duración.
Una persona SIN datos recibe el mismo ZIP con todos los conteos en cero, nunca un 404: la ruta no revela si la persona existe.
PII. Redactada por defecto: los números de teléfono salen como *** más los últimos cuatro dígitos, el texto libre (SMS, correo, conversación de IA, transcripción) pasa por la redacción de PII, y account_number y debtor_name se omiten. include_pii: true pide la exportación sin redactar y queda auditada como las demás exportaciones sin procesar.
Quién. Una clave dd_ o un inicio de sesión del dashboard con el rol account_admin. Un member recibe 403 role_forbidden (no una respuesta enmascarada), con o sin include_pii.
Indicador. Detrás de privacy_subject_export_enabled, desactivado por defecto: 503 subject_export_disabled y no se lee nada.
Rastro. Cada solicitud escribe un evento requested append-only ANTES de leer y uno delivered cuando se almacena el archivo; la persona se registra solo como HMAC. Si no se puede registrar la solicitud, no se exporta nada (503 audit_unavailable).
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
phone | string | no | Número E.164. |
consumer_ref | string | no | |
include_pii | boolean | no | Exportación sin redactar; requiere una clave dd_ o un inicio de sesión account_admin, y queda auditada. |
json
{
"phone": "+13055550123",
"include_pii": false
}Respuestas
202
La exportación se produjo; el archivo está en url.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
subject_export | object (SubjectExport) | sí | |
subject_export.id | string (uuid) | sí | El evento requested de la pista de auditoría. |
subject_export.status | done | sí | |
subject_export.include_pii | boolean | sí | |
subject_export.url | string,null | no | URL firmada de corta duración del ZIP. |
subject_export.url_expires_at | string,null (date-time) | no | |
subject_export.manifest | object | sí | Mismo contenido que manifest.json dentro del ZIP. |
subject_export.manifest.subject | object | sí | |
subject_export.manifest.subject.kind | phone · consumer_ref | no | |
subject_export.manifest.subject.hash | string | no | HMAC del sujeto, en hexadecimal. |
subject_export.manifest.include_pii | boolean | sí | |
subject_export.manifest.generated_at | string (date-time) | no | |
subject_export.manifest.row_cap_per_category | integer | no | |
subject_export.manifest.categories | object | sí | |
subject_export.manifest.truncated | arreglo de string | sí | |
subject_export.manifest.omitted | arreglo de object | sí | |
subject_export.manifest.omitted[].item | string | no | |
subject_export.manifest.omitted[].reason | string | no |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}503
subject_export_disabled (el indicador está apagado para este tenant), audit_unavailable (la solicitud no pudo registrarse, no se exportó nada) o storage_unavailable (el almacén de exportaciones no aceptó el archivo).
json
{
"error": {
"code": "subject_export_disabled",
"message": "subject exports are not enabled for this tenant"
}
}Consultar qué eliminaría borrar a una persona y qué debe conservarse
http
POST /v1/privacy/erasure-requestsPara UNA persona indicada por phone (E.164) o consumer_ref (exactamente uno), busca lo que el tenant guarda de ella (teléfono mediante índice ciego) y clasifica cada categoría: qué se anonimizaría y qué se CONSERVA, con el motivo y la fecha hasta la que se mantiene (el mínimo de registros del tenant, el legal_hold o la fecha de retención de una grabación, la supresión conservada como hash). Las copias que se derivan de las filas (la copia analítica en ClickHouse, el audio bajo Object Lock) se declaran, no se ocultan.
Esta versión es una simulación (dry run). Lee, clasifica y registra la solicitud; no cambia ninguna fila. dry_run vale true por defecto; dry_run: false responde 422 erasure_execution_unavailable.
La supresión se mantiene. Las inscripciones en DNC, los registros de cese y desistimiento y los consentimientos revocados nunca se cuentan como borrables.
Quién. Una clave dd_ o un inicio de sesión del dashboard con el rol account_admin (403 role_forbidden en caso contrario).
Indicador. Detrás de privacy_erasure_enabled, apagado por defecto y por tenant: 403 privacy_erasure_disabled y no se escribe nada.
Rastro. Cada solicitud escribe una fila privacy_requests de solo anexado (quién, cuándo, la persona únicamente como HMAC, el informe). Si no puede registrarse, no se devuelve ningún informe (503 audit_unavailable).
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
phone | string | no | Número E.164. |
consumer_ref | string | no | |
dry_run | boolean | no | Solo true está disponible en esta versión. |
json
{
"phone": "+13055550123",
"dry_run": true
}Respuestas
202
El informe de la simulación (dry run).
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
erasure_request | object (ErasureRequest) | sí | |
erasure_request.id | string (uuid) | sí | La fila privacy_requests. |
erasure_request.dry_run | boolean | sí | |
erasure_request.report | object | sí | |
erasure_request.report.subject | object | sí | |
erasure_request.report.subject.kind | phone · consumer_ref | no | |
erasure_request.report.subject.hash | string | no | HMAC del sujeto, en hexadecimal. |
erasure_request.report.dry_run | boolean | sí | |
erasure_request.report.generated_at | string (date-time) | no | |
erasure_request.report.records_floor_months | integer | sí | |
erasure_request.report.floor_basis | declared · default_max_of_supported_types | no | |
erasure_request.report.categories | arreglo de object | sí | |
erasure_request.report.categories[].category | string | no | |
erasure_request.report.categories[].tables | arreglo de string | no | |
erasure_request.report.categories[].found | integer | no | |
erasure_request.report.categories[].would_anonymize | integer | no | |
erasure_request.report.categories[].action | anonymize · none | no | |
erasure_request.report.categories[].events | integer | no | Solo conversaciones de IA: sus eventos. |
erasure_request.report.categories[].retained | arreglo de object | no | |
erasure_request.report.derived_copies | arreglo de object | sí | |
erasure_request.report.derived_copies[].item | string | no | |
erasure_request.report.derived_copies[].status | string | no | |
erasure_request.report.derived_copies[].until | string,null | no | |
erasure_request.report.derived_copies[].reason | string | no | |
erasure_request.report.execution | not_available | sí |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}422
erasure_execution_unavailable: dry_run: false aún no está disponible.
429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}503
audit_unavailable: la solicitud no pudo registrarse, por lo que no se produjo ningún informe.
Encolar una exportación masiva de grabaciones
http
POST /v1/recording-exportsEncola una exportación asíncrona de las grabaciones que coinciden con los mismos filtros que GET /v1/recordings. Devuelve 202 con un job_id; consulta periódicamente GET /v1/recording-exports/{job_id} para obtener la URL firmada del archivo. La fila del trabajo de exportación es evidencia duradera de quién extrajo grabaciones en bloque y nunca se elimina.
Un rango explícito from/to más amplio que el límite de la plataforma (recording_export_max_range_days, 366 días por defecto) — o un from muy antiguo sin límite superior — se rechaza con 422 range_too_large. Una solicitud sin límites (sin from) todavía se acepta, pero una selección con más filas que el límite de filas de la plataforma (recording_export_max_rows, 50 000 por defecto) se rechaza con 422 export_too_large, nunca se corta a medias: una exportación done siempre incluye TODAS las filas que coinciden. El error incluye max_rows y matching_rows; acota los filtros y reintenta.
Repetir una solicitud idéntica dentro de la ventana de deduplicación (recording_export_dedup_ttl_seconds, 900 por defecto) devuelve el MISMO trabajo completado — con una URL recién firmada — en lugar de regenerar el archivo, de modo que un reintento o un doble clic se responde al instante. Envía refresh: true para forzar una exportación nueva. La ventana es corta a propósito: las cargas de los medios de grabación llegan de forma asíncrona, por lo que una caché de larga duración de una ventana pasada podría omitir una grabación que llega tarde. Un trabajo en caché cuyo archivo ya no está en el almacén de exportaciones no se reutiliza: la solicitud lo regenera.
El archivo se escribe en un almacén de exportaciones dedicado, nunca en el almacén de grabaciones (cuya retención lo sellaría): contiene números de teléfono y referencias de cuenta, y el almacén de exportaciones lo hace caducar. La fila del trabajo permanece; cuando su archivo ya no existe, GET /v1/recording-exports/{job_id} informa status: expired.
Una sola exportación EN GENERACIÓN por tenant a la vez: mientras se produce una exportación, cualquier otra solicitud de generación de ese tenant se rechaza con 429 export_in_progress. Reintentar cuando aterriza la exportación en curso es barato: una repetición idéntica toma entonces la vía de deduplicación anterior y responde desde el archivo que produjo. Entregar un archivo ya producido nunca está limitado.
PII (core#2018, ADR #240). El CSV dentro del archivo está redactado por defecto: el texto libre como account_ref que contiene un número de tarjeta de pago, un SSN, un número de cuenta bancaria o un PIN/contraseña sale enmascarado, conservando como máximo los últimos cuatro dígitos. include_pii: true pide el archivo SIN redacción; requiere una clave dd_ o un inicio de sesión del dashboard con el rol account_admin (403 role_forbidden en caso contrario), queda auditado y produce su propio archivo — una solicitud sin redacción nunca reutiliza (ni alimenta) una redactada dentro de la ventana de deduplicación. GET /v1/recording-exports/{job_id} de un trabajo sin redacción responde 403 role_forbidden a cualquier otro.
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
from | string (date-time) | no | |
to | string (date-time) | no | |
agent_id | string (uuid) | no | |
campaign_id | string (uuid) | no | |
disposition | string | no | |
q | string | no | |
refresh | boolean | no | Omite la ventana de deduplicación de corta duración y fuerza una exportación nueva aunque acabe de producirse una idéntica. |
include_pii | boolean | no | true produce el archivo SIN redacción de PII (ADR #240). Requiere una clave dd_ o un inicio de sesión account_admin: 403 role_forbidden en caso contrario. Ausente o false ⇒ redactado. Auditado. |
json
{
"from": "2026-06-01T00:00:00Z",
"to": "2026-06-30T23:59:59Z",
"campaign_id": "3f2c1b0a-9d8e-4c7b-a6f5-4e3d2c1b0a99"
}Respuestas
202
Exportación aceptada y encolada.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
export | object (RecordingExportJob) | sí | |
export.job_id | string (uuid) | sí | |
export.status | queued · running · done · failed · expired | sí | expired se informa al leer un trabajo done cuyo archivo ya no está en el almacén de exportaciones; regenéralo con una nueva solicitud de exportación. |
export.pii | redacted · raw | no | Indica si el archivo se produjo con la PII del catálogo cerrado de ADR #240 redactada (por defecto) o sin redactar (include_pii: true). |
export.url | string | null | no | URL firmada del archivo; presente solo cuando status es done. |
export.expires_at | string (date-time) | null | no | |
export.row_count | integer | null | no | |
export.error | string | null | no | |
export.created_at | string (date-time) | no | |
export.updated_at | string (date-time) | no |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}422
range_too_large: el rango de exportación solicitado excede el límite de la plataforma (recording_export_max_range_days, 366 días por defecto). Acota la ventana from/to y reintenta.
export_too_large: los filtros coinciden con más grabaciones que el límite de filas de la plataforma (recording_export_max_rows, 50 000 por defecto). El error añade max_rows y matching_rows; no se crea ningún trabajo. Acota los filtros y reintenta.
range_too_large
json
{
"error": {
"code": "range_too_large",
"message": "requested range exceeds the 366-day export limit"
}
}export_too_large
json
{
"error": {
"code": "export_too_large",
"message": "the filters match 120000 recordings, above the 50000-row export limit; narrow the filters and retry",
"max_rows": 50000,
"matching_rows": 120000
}
}429
Este tenant ya tiene una exportación en generación. Solo se ejecuta una exportación en generación por tenant a la vez; reintenta cuando termine (una repetición idéntica se sirve entonces desde el archivo producido sin regenerarlo). Un reclamo cuya solicitud murió se libera automáticamente tras recording_export_claim_stale_seconds (900 por defecto).
json
{
"error": {
"code": "export_in_progress",
"message": "an export is already running for this tenant; retry when it completes"
}
}503
El almacén de exportaciones no pudo aceptar el archivo (inalcanzable, mal configurado o rechaza la escritura). El almacén de exportaciones es un almacén propio, independiente del almacén de audio de grabaciones; se devuelve 503 siempre que falla, nunca 500.
json
{
"error": {
"code": "storage_unavailable",
"message": "recording export storage is not configured"
}
}Consultar el estado de un trabajo de exportación de grabaciones
http
GET /v1/recording-exports/{job_id}Devuelve el estado del trabajo; cuando es done, url es una URL firmada de corta duración del archivo (expires_at marca su caducidad). job_id de otro tenant o desconocido → 404.
expired: el trabajo se completó pero su archivo ya no está en el almacén de exportaciones (el almacén hace caducar los archivos según su propio calendario). url y expires_at son null; envía un nuevo POST /v1/recording-exports para regenerarlo. El trabajo en sí se conserva como evidencia de quién exportó qué. Un trabajo creado con include_pii: true (pii: raw) es 403 role_forbidden para un llamante sin el rol account_admin. Si no se puede alcanzar el almacén, el trabajo permanece done con su URL firmada — expired solo se informa cuando se sabe que el archivo ya no existe.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
job_id | path | string (uuid) | sí | UUID del trabajo de exportación. Los ids mal formados o de otro tenant se leen como 404. |
Respuestas
200
El trabajo de exportación.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
export | object (RecordingExportJob) | sí | |
export.job_id | string (uuid) | sí | |
export.status | queued · running · done · failed · expired | sí | expired se informa al leer un trabajo done cuyo archivo ya no está en el almacén de exportaciones; regenéralo con una nueva solicitud de exportación. |
export.pii | redacted · raw | no | Indica si el archivo se produjo con la PII del catálogo cerrado de ADR #240 redactada (por defecto) o sin redactar (include_pii: true). |
export.url | string | null | no | URL firmada del archivo; presente solo cuando status es done. |
export.expires_at | string (date-time) | null | no | |
export.row_count | integer | null | no | |
export.error | string | null | no | |
export.created_at | string (date-time) | no | |
export.updated_at | string (date-time) | no |
401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Obtener los metadatos completos de una grabación
http
GET /v1/recordings/{id}La vista de evidencia completa: base del consentimiento, la instantánea congelada gates_at_dial, el decision_ref citable dentro de la cadena de auditoría, los metadatos del almacén de objetos y el registro de retención legal. Sin bytes de audio.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string (uuid) | sí | UUID del recurso. Los ids mal formados o de otro tenant se leen como 404. |
Respuestas
200
La grabación.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
recording | any | sí |
401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Emitir una URL de reproducción firmada de corta duración
http
POST /v1/recordings/{id}/playback-urlDevuelve una URL firmada de corta duración del almacén de objetos (Cloudflare R2 / compatible con S3) que el navegador consulta directamente — el audio NUNCA pasa por el servidor de la API. expires_at es la caducidad estricta de la URL (minutos). Cada emisión queda auditada.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string (uuid) | sí | UUID del recurso. Los ids mal formados o de otro tenant se leen como 404. |
Respuestas
200
Una URL recién firmada.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
url | string | sí | URL firmada de corta duración del almacén de objetos. |
expires_at | string (date-time) | sí |
json
{
"url": "https://r2.example.com/rec/0e9f8a7b...?X-Amz-Signature=...",
"expires_at": "2026-07-09T18:05:00Z"
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409 — Conflict
Transición de estado no válida, conflicto de unicidad, o un asiento que está ocupado/reservado.
json
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Emitir una URL de corta duración para el audio SIN REDACTAR
http
POST /v1/recordings/{id}/original-urlLa vista por defecto (POST /v1/recordings/{id}/playback-url) reproduce la copia con la PII redactada. Esta es la única vía al original. Requiere una clave de tenant dd_ o un inicio de sesión del dashboard con el rol account_admin (cualquier otro es 403 role_forbidden) y un reason no vacío (como máximo 500 caracteres). El actor y el motivo se escriben en un registro de acceso de solo anexado ANTES de devolver la URL.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string (uuid) | sí | UUID del recurso. Los ids mal formados o de otro tenant se leen como 404. |
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
reason | string | sí |
json
{
"reason": "Dispute 2026-0417: customer denies the payment promise"
}Respuestas
200
Una URL recién firmada del audio original.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
url | string | sí | URL firmada de corta duración del almacén de objetos. |
expires_at | string (date-time) | sí |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409 — Conflict
Transición de estado no válida, conflicto de unicidad, o un asiento que está ocupado/reservado.
json
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}La redacción de PII del audio de una grabación
http
GET /v1/recordings/{id}/redactionTramos (solo tiempo y categoría, nunca el valor), método y versión. 404 cuando la grabación no tiene redacción.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string (uuid) | sí | UUID del recurso. Los ids mal formados o de otro tenant se leen como 404. |
Respuestas
200
La redacción.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
redaction | object (RecordingRedaction) | no | |
redaction.recording_id | string (uuid) | no | |
redaction.method | silence · beep | no | |
redaction.segments | arreglo de RecordingRedactionSegment | no | |
redaction.segments[].start_ms | integer | sí | |
redaction.segments[].end_ms | integer | sí | |
redaction.segments[].category | card · ssn · bank_account · pin | sí | |
redaction.redacted_ms | integer | no | |
redaction.version | integer | no | |
redaction.codec | string | no | |
redaction.requested_by | string | no | |
redaction.created_at | string (date-time) | no | |
redaction.updated_at | string (date-time) | no |
401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}Redactar tramos con PII del audio de una grabación
http
POST /v1/recordings/{id}/redactionMismo control por rol que original-url. Los tramos se unen a los ya almacenados y la copia redactada se reconstruye a partir del original (idempotente, version sube cuando se añadió algo nuevo). Solo grabaciones WAV: otro formato es 422 unsupported_codec.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string (uuid) | sí | UUID del recurso. Los ids mal formados o de otro tenant se leen como 404. |
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
method | silence · beep | no | |
segments | arreglo de RecordingRedactionSegment | sí | |
segments[].start_ms | integer | sí | |
segments[].end_ms | integer | sí | |
segments[].category | card · ssn · bank_account · pin | sí |
Respuestas
200
La redacción después de la llamada.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
redaction | object (RecordingRedaction) | no | |
redaction.recording_id | string (uuid) | no | |
redaction.method | silence · beep | no | |
redaction.segments | arreglo de RecordingRedactionSegment | no | |
redaction.segments[].start_ms | integer | sí | |
redaction.segments[].end_ms | integer | sí | |
redaction.segments[].category | card · ssn · bank_account · pin | sí | |
redaction.redacted_ms | integer | no | |
redaction.version | integer | no | |
redaction.codec | string | no | |
redaction.requested_by | string | no | |
redaction.created_at | string (date-time) | no | |
redaction.updated_at | string (date-time) | no |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}409 — Conflict
Transición de estado no válida, conflicto de unicidad, o un asiento que está ocupado/reservado.
json
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}422 — Unprocessable
La forma es correcta, la semántica no (regla de dominio incumplida).
json
{
"error": {
"code": "unprocessable",
"message": "scheduled_at is closer than the minimum notice for a callback (10 minutes unless the tenant's callback_min_notice_seconds says otherwise)"
}
}503
storage_unavailable: el almacén de grabaciones no puede leer el original ni escribir la copia redactada en este momento. No se persistió nada; reintenta más tarde.
La transcripción de una grabación
http
GET /v1/recordings/{id}/transcriptLa transcripción de voz a texto de UNA grabación, con sus segmentos temporizados descifrados ordenados por seq. Son datos personales sin procesar de una llamada, por lo que solo un inicio de sesión del dashboard (ddu_) con puesto de supervisor o administrador, o un inicio de sesión account_admin sin puesto, puede leerla: una clave dd_, un token de widget y un puesto de agente son 403 role_forbidden. Cada entrega añade un evento de lectura (quién, cuándo, qué grabación, cuántos segmentos; nunca el texto) en la misma transacción que la lectura.
El texto es la vista por defecto con la PII redactada (redacted: true): los números de tarjeta, los números de seguro social, las cuentas bancarias y los PIN/contraseñas se sustituyen por máscaras o [REDACTED]. El original solo es accesible mediante POST /v1/recordings/{id}/transcript/original.
404 cuando la grabación no tiene transcripción en este tenant: pertenece a otro tenant, su audio fue purgado (la purga elimina con él la transcripción), no era elegible, o el id está mal formado. Una grabación bajo legal_hold conserva su transcripción.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string (uuid) | sí | UUID del recurso. Los ids mal formados o de otro tenant se leen como 404. |
Respuestas
200
La transcripción.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
transcript | object (RecordingTranscript) | sí | |
transcript.recording_id | string (uuid) | sí | |
transcript.redacted | boolean | sí | true para la vista por defecto (PII redactada); false solo en el original auditado. |
transcript.provider | string | sí | Proveedor de voz a texto que la produjo. |
transcript.model | string | sí | |
transcript.language | string,null | no | Idioma que informó el proveedor, cuando informó alguno. |
transcript.audio_seconds | integer | sí | Segundos de audio facturados. |
transcript.segment_count | integer | sí | |
transcript.created_at | string (date-time) | sí | |
transcript.segments | arreglo de object | sí | |
transcript.segments[].seq | integer | sí | |
transcript.segments[].channel | integer | sí | Canal de audio: 0 o 1. |
transcript.segments[].start_ms | integer | sí | |
transcript.segments[].end_ms | integer | sí | |
transcript.segments[].text | string | sí |
401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}503
El evento de lectura no pudo registrarse, por lo que no se devolvió ningún texto.
La transcripción SIN REDACTAR de una grabación
http
POST /v1/recordings/{id}/transcript/originalLa vista por defecto (GET /v1/recordings/{id}/transcript) tiene la PII redactada. Esta es la única vía al texto original, guardado cifrado para auditoría (Reg F). Requiere una clave de tenant dd_ o un inicio de sesión del dashboard con el rol account_admin (cualquier otro es 403 role_forbidden) y un reason no vacío (como máximo 500 caracteres). El actor, el motivo y el número de segmentos (nunca el texto) se escriben en un registro de acceso de solo anexado ANTES de devolver el texto: si la fila no puede escribirse, no sale ningún texto (503). 404 igual que en la vista por defecto.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string (uuid) | sí | UUID del recurso. Los ids mal formados o de otro tenant se leen como 404. |
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
reason | string | sí |
json
{
"reason": "Dispute 2026-0417: customer denies the payment promise"
}Respuestas
200
La transcripción, con redacted: false.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
transcript | object (RecordingTranscript) | sí | |
transcript.recording_id | string (uuid) | sí | |
transcript.redacted | boolean | sí | true para la vista por defecto (PII redactada); false solo en el original auditado. |
transcript.provider | string | sí | Proveedor de voz a texto que la produjo. |
transcript.model | string | sí | |
transcript.language | string,null | no | Idioma que informó el proveedor, cuando informó alguno. |
transcript.audio_seconds | integer | sí | Segundos de audio facturados. |
transcript.segment_count | integer | sí | |
transcript.created_at | string (date-time) | sí | |
transcript.segments | arreglo de object | sí | |
transcript.segments[].seq | integer | sí | |
transcript.segments[].channel | integer | sí | Canal de audio: 0 o 1. |
transcript.segments[].start_ms | integer | sí | |
transcript.segments[].end_ms | integer | sí | |
transcript.segments[].text | string | sí |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}503
El acceso no pudo registrarse, por lo que no se devolvió ningún texto.
Buscar una palabra o frase en las transcripciones de una ventana
http
GET /v1/transcripts/searchBusca las grabaciones de este tenant cuya transcripción contiene q, de la más reciente a la más antigua. Las palabras de q se buscan en un índice CIEGO (un hash con clave que incluye el tenant: la misma palabra produce un hash distinto en dos tenants, y ninguna palabra se guarda en claro); solo se descifran los segmentos candidatos de las grabaciones candidatas, y una frase se verifica sobre el texto descifrado: las palabras deben aparecer seguidas dentro de UN solo segmento. La coincidencia es exacta sobre la palabra normalizada (se ignoran mayúsculas, acentos y puntuación; sin derivación morfológica ni sinónimos). Una grabación cuyo audio fue purgado no tiene transcripción y nunca aparece; una bajo legal_hold sí.
from y to son obligatorios, from debe ser anterior a to y la ventana no puede exceder 31 días (400 en caso contrario); q debe contener al menos una y como máximo 8 palabras. Una llamada examina como máximo 100 grabaciones candidatas y 500 segmentos: cuando ese tope detiene la búsqueda antes de llenar la página, capped es true y next_cursor continúa desde donde se detuvo. Son datos personales sin procesar de llamadas, así que el control es el de la transcripción individual: un inicio de sesión del dashboard (ddu_) con puesto de supervisor o administrador, o un inicio de sesión account_admin sin puesto; una clave dd_, un token de widget y un puesto de agente son 403 role_forbidden.
Toda búsqueda que se ejecuta añade un evento (quién, cuándo, el HASH de la consulta, la ventana, los filtros, cuántas grabaciones se examinaron y se devolvieron; nunca el texto) en la misma transacción: si no puede escribirse no hay resultado (503).
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
q | query | string | sí | La palabra o frase (de 1 a 8 palabras). |
from | query | string (date-time) | sí | Cota inferior RFC 3339 del inicio de la grabación (inclusiva). |
to | query | string (date-time) | sí | Límite superior RFC 3339 del inicio de la grabación; como máximo 31 días después de from. |
agent_id | query | string (uuid) | no | Solo grabaciones de este agente. |
campaign_id | query | string (uuid) | no | Solo grabaciones de esta campaña. |
limit | query | integer | no | Tamaño de página (un valor por encima del techo se recorta, no se rechaza). |
cursor | query | string | no | Cursor opaco por conjunto de claves (keyset), el next_cursor de la página anterior. |
Respuestas
200
Las grabaciones que coinciden, con los fragmentos alrededor de las coincidencias.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
results | arreglo de object | sí | |
results[].recording_id | string (uuid) | sí | |
results[].started_at | string (date-time) | sí | |
results[].agent_id | string,null (uuid) | sí | |
results[].campaign_id | string,null (uuid) | sí | |
results[].matches | arreglo de object | sí | |
results[].matches[].seq | integer | sí | |
results[].matches[].channel | integer | sí | Canal de audio: 0 o 1. |
results[].matches[].start_ms | integer | sí | |
results[].matches[].end_ms | integer | sí | |
results[].matches[].fragment | string | sí | El texto del segmento alrededor de la coincidencia (unos 40 caracteres de contexto a cada lado). |
next_cursor | string,null | sí | Cursor opaco para la página siguiente; null cuando no queda nada más por examinar. |
capped | boolean | sí | Verdadero cuando el tope por llamada de grabaciones o segmentos detuvo la búsqueda antes de llenar la página. |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}429 — TranscriptSearchThrottled
Más de 30 búsquedas (por defecto) en la ventana fija para este usuario del dashboard (core#2505). code rate_limited, con Retry-After. El presupuesto es de la persona, así que un colega del mismo tenant no queda limitado por él. Un rechazo por rol (403) no lo consume.
json
{
"error": {
"code": "rate_limited",
"message": "transcript search rate limit exceeded; retry later"
}
}503
El evento de búsqueda no pudo registrarse, por lo que no se devolvió ningún resultado.
Establecer o levantar la retención legal de una grabación
http
PUT /v1/recordings/{id}/legal-holdReemplazo completo e idempotente del subrecurso de retención legal (legal hold) de la grabación. active: true EXIGE un reason no vacío. Una grabación retenida queda exenta de toda purga hasta que se levante la retención, incluso pasado su mínimo de retención. Ámbito de gestión del tenant: solo una clave de API de tenant dd_ — un token de navegador ddw_ recibe 403. Establecer la retención queda auditado (set_by, set_at).
Reglas de rol (core#2438): una clave dd_ o un inicio de sesión account_admin; un inicio de sesión member recibe 403 role_forbidden (ajuste por tenant compliance_config_admin_only, activo por defecto). El GET de este recurso no se ve afectado.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string (uuid) | sí | UUID del recurso. Los ids mal formados o de otro tenant se leen como 404. |
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
active | boolean | sí | |
reason | string | no | OBLIGATORIO (no vacío) cuando active es true; se ignora cuando es false. |
json
{
"active": true,
"reason": "Litigation hold — Apex Recovery v. Doe, matter #2026-0417"
}Respuestas
200
La grabación con la retención actualizada.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
recording | any | sí |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Obtener la política de grabación del tenant
http
GET /v1/recording-policyRespuestas
200
La política efectiva (valores por defecto materializados en la primera lectura).
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
recording_policy | object (RecordingPolicy) | sí | |
recording_policy.enabled | boolean | sí | Indica si las llamadas nuevas se graban por defecto. |
recording_policy.retention_months | integer | sí | Retención en meses (39 por defecto). El valor más bajo aceptado es el mínimo efectivo del tenant (GET /v1/business-types): 39 hasta que declara sus tipos. El más alto aceptado es max(3, floor) más 12 meses por cada bloque de retención adicional comprado (core#2423); pedir más es 422. |
recording_policy.channels | mono · stereo | sí | |
recording_policy.format | wav · opus | sí | Códec de almacenamiento para grabaciones NUEVAS. Nunca recodifica lo que ya está almacenado, y cambiarlo nunca refecha una retención: ambos se congelan por grabación en su creación. opus es el valor por defecto con el que nace un nuevo tenant (divide la huella retenida entre ~8); wav es el valor que se mantiene para un tenant sujeto a una obligación regulatoria, contractual o de litigio de conservar audio sin comprimir, y conlleva un recargo — véase RecordingPolicyWrite.format. Un tenant que ya está en wav se queda en wav: la migración 20260907000100 primero materializa una fila de política wav para cada tenant que existe el 2026-09-07, de modo que el nuevo valor por defecto alcanza solo a los tenants creados desde esa fecha. Sin ese paso alcanzaría también a todo tenant más antiguo que nunca hubiera llamado a este endpoint, porque su fila se crea de forma perezosa. |
recording_policy.updated_at | string (date-time) | no |
401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Reemplazar la política de grabación del tenant
http
PUT /v1/recording-policyReemplazo completo de la política singleton por tenant. retention_months se ajusta al mínimo EFECTIVO del tenant (39 hasta que declara sus tipos de negocio; véase GET /v1/business-types) — un valor menor es 422 unprocessable. También tiene un TECHO (core#2423): lo que incluye el plan (max(3, floor) meses) más 12 meses por cada bloque de retención adicional que el tenant haya comprado — un valor mayor es 422 unprocessable también. Una política ya almacenada por encima de su techo conserva su valor y puede guardarse de nuevo con ese valor o uno menor, nunca aumentarse. Cambiar la política NUNCA refecha las grabaciones existentes: retained_until se congela por fila en la creación (doctrina de la instantánea en el momento). Ámbito de gestión del tenant (solo dd_; ddw_ → 403).
Reglas de rol (core#2438): una clave dd_ o un inicio de sesión account_admin; un inicio de sesión member recibe 403 role_forbidden (ajuste por tenant compliance_config_admin_only, activo por defecto). El GET de este recurso no se ve afectado.
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
enabled | boolean | no | |
retention_months | integer | no | Por debajo del mínimo efectivo del tenant (39 hasta que declara sus tipos; GET /v1/business-types) → 422 unprocessable. |
channels | mono · stereo | no | |
format | wav · opus | no | Códec de almacenamiento para grabaciones NUEVAS; fuera del vocabulario → 400 bad_request que nombra el campo (el escalón 422 se reserva para el mínimo de retención, que es un rechazo por regla de negocio). opus es el valor por defecto y la vía económica. wav es la excepción por tenant en dos casos y solo dos — una obligación regulatoria, contractual o de litigio de conservar audio sin comprimir, o que el cliente lo pida expresamente — y se facturará: el precio está fijado (DialerDigital/macro-tasks#475, 2026-09-06) en una tarifa plana de 5,00 USD por puesto y mes mientras el formato de un tenant sea wav, contado en el último instante del mes cerrado, sin exención para el tenant sujeto a una obligación legal (decisión del fundador sobre el códec en freeswitch ADR.md #15, 2026-09-04). La línea wav_surcharge SÍ se emite: el cierre mensual la factura desde DialerDigital/core#779 (ADR #81), sobre el códec que el tenant tenía en el límite del periodo — leído del historial de formatos de solo anexado, no de la fila de política tal como está hoy — y los mismos puestos facturables que usa la línea seats. Cambiar el formato afecta solo a las grabaciones hechas DESPUÉS del cambio. Nada de lo almacenado se transcodifica ni se refecha: las grabaciones están bajo Object Lock con un mínimo de retención de 39 meses congelado por fila. Este enum está reflejado por Recordings.Policy.writable_formats/0 y puede ser legítimamente MÁS ESTRECHO que el del lado de lectura: admitir un códec en LECTURA es un hecho de catálogo, acuñar uno en ESCRITURA es una postura de producto. Fue [wav] hasta DialerDigital/core#731 y se amplió en un solo cambio junto con writable_formats/0 y el DEFAULT de la columna, porque un valor por defecto que nadie puede además ESTABLECER es una compuerta entreabierta. El recording.format del chart de FreeSWITCH NO es este eje y no elige el códec: es por RELEASE mientras que este es por TENANT, y su guarda todavía rechaza cualquier valor distinto de wav (deploy/chart/templates/_helpers.tpl). Lo que llega al disco es la extensión de la clave de objeto que core entrega a uuid_record (Recordings.object_key/4), derivada de este campo. HISTORIAL, para no resucitar motivos retirados. (a) Hasta DialerDigital/freeswitch#85 este enum se justificaba porque el sidecar de carga resolvía UNA SOLA extensión — un tenant en Opus haría que FreeSWITCH escribiera .opus, el sidecar no encontraría ningún archivo, y nada se pondría en rojo. ESO ESTÁ ARREGLADO: el sidecar descubre todo el conjunto manejado y pone en cuarentena lo que quede fuera, nombrándolo (freeswitch ADR.md #16). A propósito sin números de línea aquí: el texto anterior apuntaba a uploader.sh:94, que esa misma corrección dejó en blanco. (b) NO volver a añadir «voice-ai no puede leer no-WAV»: medido y refutado (DialerDigital/voice-ai#70, cerrado como not-planned 2026-09-04) — voice-ai consume L16 sin procesar sobre el media fork y nunca lee una grabación almacenada, así que el códec de almacenamiento y lo que voice-ai ingiere son variables independientes. |
json
{
"enabled": true,
"retention_months": 39,
"channels": "stereo"
}Respuestas
200
La política actualizada.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
recording_policy | object (RecordingPolicy) | sí | |
recording_policy.enabled | boolean | sí | Indica si las llamadas nuevas se graban por defecto. |
recording_policy.retention_months | integer | sí | Retención en meses (39 por defecto). El valor más bajo aceptado es el mínimo efectivo del tenant (GET /v1/business-types): 39 hasta que declara sus tipos. El más alto aceptado es max(3, floor) más 12 meses por cada bloque de retención adicional comprado (core#2423); pedir más es 422. |
recording_policy.channels | mono · stereo | sí | |
recording_policy.format | wav · opus | sí | Códec de almacenamiento para grabaciones NUEVAS. Nunca recodifica lo que ya está almacenado, y cambiarlo nunca refecha una retención: ambos se congelan por grabación en su creación. opus es el valor por defecto con el que nace un nuevo tenant (divide la huella retenida entre ~8); wav es el valor que se mantiene para un tenant sujeto a una obligación regulatoria, contractual o de litigio de conservar audio sin comprimir, y conlleva un recargo — véase RecordingPolicyWrite.format. Un tenant que ya está en wav se queda en wav: la migración 20260907000100 primero materializa una fila de política wav para cada tenant que existe el 2026-09-07, de modo que el nuevo valor por defecto alcanza solo a los tenants creados desde esa fecha. Sin ese paso alcanzaría también a todo tenant más antiguo que nunca hubiera llamado a este endpoint, porque su fila se crea de forma perezosa. |
recording_policy.updated_at | string (date-time) | no |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}422 — Unprocessable
La forma es correcta, la semántica no (regla de dominio incumplida).
json
{
"error": {
"code": "unprocessable",
"message": "scheduled_at is closer than the minimum notice for a callback (10 minutes unless the tenant's callback_min_notice_seconds says otherwise)"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Leer los tipos de negocio declarados del tenant y el mínimo de retención de grabaciones que implican
http
GET /v1/business-typesADR #246 (core#2065). Responde tres cosas a la vez para que un cliente nunca tenga que combinarlas: los tipos que el tenant DECLARÓ (declared, null cuando no declaró nada), el vocabulario CERRADO que puede declarar con el mínimo de audio y la fuente legal de cada tipo (supported), y el mínimo EFECTIVO con el o los tipos que lo imponen (retention_floor).
retention_floor.months es legal_floor_months + safety_margin_months: el mínimo de audio MÁXIMO de los tipos declarados más el margen de la plataforma (3 meses, heredado de la planeación, no es una cifra legal). NUNCA es 0: un tipo cuya ley no exige audio igualmente conserva la retención incluida de la plataforma. Un tenant que no declaró nada recibe el máximo de los tipos soportados (basis: undeclared_maximum) — omitir la declaración nunca puede bajar el mínimo. Un mínimo que no puede resolverse es 503, nunca una cifra adivinada.
Cualquier principal de consola lee (una clave dd_ o un inicio de sesión del dashboard de cualquier rol). Las fuentes son solo normas federales de EE. UU.; la ley estatal NO está cubierta (ADR #246, «Límite de la investigación»).
Respuestas
200
La declaración y el mínimo efectivo.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
business_types | object (BusinessTypes) | sí | |
business_types.declared | array,null | sí | Los tipos que el tenant declaró, ordenados. null = no declaró nada. |
business_types.supported | arreglo de BusinessTypeFloor | sí | El vocabulario cerrado que un tenant puede declarar, cada uno con su mínimo de audio y su fuente. |
business_types.supported[].type | cobranza · telemarketing · servicio_cliente · financiera_valores · hipotecas_servicing | sí | |
business_types.supported[].audio_floor_months | integer | sí | Meses que la ley exige conservar el AUDIO de una llamada grabada de este tipo (0 cuando la norma no exige el audio). Sin el margen de la plataforma. |
business_types.supported[].source | string | sí | La fuente legal, tal como la cita ADR #246 (solo normas federales de EE. UU.). |
business_types.retention_floor | object | sí | El mínimo de retención de grabaciones vigente para las grabaciones NUEVAS. |
business_types.retention_floor.months | integer | sí | legal_floor_months + safety_margin_months. Nunca por debajo del margen; nunca 0. |
business_types.retention_floor.legal_floor_months | integer | sí | El mínimo de audio máximo entre los tipos declarados (entre todos los tipos soportados cuando no se declara nada). |
business_types.retention_floor.safety_margin_months | integer | sí | El margen de la plataforma añadido sobre el mínimo legal (3 hoy; heredado de la planeación, no es una cifra legal). |
business_types.retention_floor.basis | declared · undeclared_maximum | sí | undeclared_maximum = el tenant no declaró nada y el mínimo es el máximo a prueba de fallos (fail-closed) de los tipos soportados. |
business_types.retention_floor.imposed_by | arreglo de BusinessTypeFloor | sí | El o los tipos cuyo mínimo de audio es el que rige, cada uno con su fuente legal. |
business_types.retention_floor.imposed_by[].type | cobranza · telemarketing · servicio_cliente · financiera_valores · hipotecas_servicing | sí | |
business_types.retention_floor.imposed_by[].audio_floor_months | integer | sí | Meses que la ley exige conservar el AUDIO de una llamada grabada de este tipo (0 cuando la norma no exige el audio). Sin el margen de la plataforma. |
business_types.retention_floor.imposed_by[].source | string | sí | La fuente legal, tal como la cita ADR #246 (solo normas federales de EE. UU.). |
401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}503
La declaración del tenant no pudo leerse, por lo que no se publica ningún mínimo.
Declarar los tipos de negocio del tenant
http
PUT /v1/business-typesReemplaza la declaración. Solo administradores de la cuenta: el llamante debe ser un inicio de sesión del dashboard (ddu_) con el rol account_admin. Una clave dd_ lee y recibe 403 role_forbidden, y también un inicio de sesión member — bajar un mínimo es una decisión de una persona que responde por ella (ADR #246, decisión 6).
business_types es OBLIGATORIO: una lista tomada del vocabulario cerrado (véase supported), o null / [] para retirar la declaración (vuelve al máximo a prueba de fallos). La lista se guarda ordenada y sin duplicados. Un tipo fuera del vocabulario es 422 unprocessable; una clave ausente, un valor que no es una lista ni null, o una clave desconocida en el cuerpo es 400 bad_request.
Todo cambio — al alza o a la baja — escribe UNA fila de auditoría de solo anexado en la misma transacción: quién, cuándo, el valor anterior y el nuevo, y el mínimo que cada uno implicaba. Declarar lo que ya está declarado no escribe nada (changed: false). Un mínimo más bajo se aplica solo a las grabaciones NUEVAS: retained_until permanece congelado por grabación y los objetos ya sellados conservan su sello.
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
business_types | array,null | sí |
json
{
"business_types": [
"cobranza"
]
}Respuestas
200
La declaración vigente ahora y lo que hizo el cambio.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
business_types | object (BusinessTypes) | sí | |
business_types.declared | array,null | sí | Los tipos que el tenant declaró, ordenados. null = no declaró nada. |
business_types.supported | arreglo de BusinessTypeFloor | sí | El vocabulario cerrado que un tenant puede declarar, cada uno con su mínimo de audio y su fuente. |
business_types.supported[].type | cobranza · telemarketing · servicio_cliente · financiera_valores · hipotecas_servicing | sí | |
business_types.supported[].audio_floor_months | integer | sí | Meses que la ley exige conservar el AUDIO de una llamada grabada de este tipo (0 cuando la norma no exige el audio). Sin el margen de la plataforma. |
business_types.supported[].source | string | sí | La fuente legal, tal como la cita ADR #246 (solo normas federales de EE. UU.). |
business_types.retention_floor | object | sí | El mínimo de retención de grabaciones vigente para las grabaciones NUEVAS. |
business_types.retention_floor.months | integer | sí | legal_floor_months + safety_margin_months. Nunca por debajo del margen; nunca 0. |
business_types.retention_floor.legal_floor_months | integer | sí | El mínimo de audio máximo entre los tipos declarados (entre todos los tipos soportados cuando no se declara nada). |
business_types.retention_floor.safety_margin_months | integer | sí | El margen de la plataforma añadido sobre el mínimo legal (3 hoy; heredado de la planeación, no es una cifra legal). |
business_types.retention_floor.basis | declared · undeclared_maximum | sí | undeclared_maximum = el tenant no declaró nada y el mínimo es el máximo a prueba de fallos (fail-closed) de los tipos soportados. |
business_types.retention_floor.imposed_by | arreglo de BusinessTypeFloor | sí | El o los tipos cuyo mínimo de audio es el que rige, cada uno con su fuente legal. |
business_types.retention_floor.imposed_by[].type | cobranza · telemarketing · servicio_cliente · financiera_valores · hipotecas_servicing | sí | |
business_types.retention_floor.imposed_by[].audio_floor_months | integer | sí | Meses que la ley exige conservar el AUDIO de una llamada grabada de este tipo (0 cuando la norma no exige el audio). Sin el margen de la plataforma. |
business_types.retention_floor.imposed_by[].source | string | sí | La fuente legal, tal como la cita ADR #246 (solo normas federales de EE. UU.). |
changed | boolean | sí | false cuando el cuerpo declaró lo que ya estaba declarado (sin fila de auditoría). |
lowered | boolean | sí | true cuando el mínimo efectivo bajó; la fila de auditoría nombra a quien lo hizo. |
previous_floor_months | integer | sí | El mínimo efectivo antes de esta llamada. |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}422 — Unprocessable
La forma es correcta, la semántica no (regla de dominio incumplida).
json
{
"error": {
"code": "unprocessable",
"message": "scheduled_at is closer than the minimum notice for a callback (10 minutes unless the tenant's callback_min_notice_seconds says otherwise)"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}503
No se pudo leer el tenant, por lo que no se declaró nada.