Skip to content

Grabaciones ​

Generado desde DialerDigital/core commit 0fc18008b3c8 — especificación openapi/dialer-v1.yaml v0.134.0. No se edita a mano; consulta Cómo se construye esta referencia. Descargar la especificación OpenAPI.

English version

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 ​

MétodoRutaResumen
PUT/v1/screen-recordings/{id}/chunks/{n}Subir un fragmento de la grabación de pantalla del puesto PROPIO
POST/v1/screen-recordings/gapsDeclarar que falta la pantalla de una llamada, y por qué
POST/v1/screen-recordings/{id}/pii-marksDeclarar qué tramos de la grabación de pantalla del puesto PROPIO mostraron PII
POST/v1/screen-recordings/playbackSolicitar ver la grabación de pantalla REDACTADA de una llamada; devuelve una URL firmada de corta duración
POST/v1/screen-recordings/original-playbackSolicitar ver la grabación de pantalla SIN REDACTAR de una llamada, con un motivo; devuelve una URL firmada de corta duración
GET/v1/recordingsListar grabaciones de llamadas (filtradas, paginadas por keyset)
POST/v1/privacy/subject-exportsExportar los datos que se conservan sobre una persona
POST/v1/privacy/erasure-requestsConsultar qué eliminaría borrar a una persona y qué debe conservarse
POST/v1/recording-exportsEncolar una exportación masiva de grabaciones
GET/v1/recording-exports/{job_id}Consultar el estado de un trabajo de exportación de grabaciones
GET/v1/recordings/{id}Obtener los metadatos completos de una grabación
POST/v1/recordings/{id}/playback-urlEmitir una URL de reproducción firmada de corta duración
POST/v1/recordings/{id}/original-urlEmitir una URL de corta duración para el audio SIN REDACTAR
GET/v1/recordings/{id}/redactionLa redacción de PII del audio de una grabación
POST/v1/recordings/{id}/redactionRedactar tramos con PII del audio de una grabación
GET/v1/recordings/{id}/transcriptLa transcripción de una grabación
POST/v1/recordings/{id}/transcript/originalLa transcripción SIN REDACTAR de una grabación
GET/v1/transcripts/searchBuscar una palabra o frase en las transcripciones de una ventana
PUT/v1/recordings/{id}/legal-holdEstablecer o levantar la retención legal de una grabación
GET/v1/recording-policyObtener la política de grabación del tenant
PUT/v1/recording-policyReemplazar la política de grabación del tenant
GET/v1/business-typesLeer los tipos de negocio declarados del tenant y el mínimo de retención de grabaciones que implican
PUT/v1/business-typesDeclarar los tipos de negocio del tenant

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

NombreEnTipoObligatorioDescripción
idpathstring (uuid)síId de la grabación, elegido por el cliente, uno por llamada.
npathintegersíNúmero de fragmento, desde 0.
call_uuidquerystringsíLa llamada que se graba; debe ser del propio solicitante.

Respuestas

200 ​

El fragmento ya estaba almacenado con los mismos bytes.

CampoTipoObligatorioDescripción
chunkobjectsí
chunk.recording_idstring (uuid)sí
chunk.seqintegersí
chunk.bytesintegersíTamaño del fragmento antes de sellarlo.
chunk.statusstored · duplicatesí
chunk.retained_untilstring (date)síHeredado de la grabación de voz de la misma llamada.

201 ​

El fragmento se selló y se almacenó.

CampoTipoObligatorioDescripción
chunkobjectsí
chunk.recording_idstring (uuid)sí
chunk.seqintegersí
chunk.bytesintegersíTamaño del fragmento antes de sellarlo.
chunk.statusstored · duplicatesí
chunk.retained_untilstring (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/gaps

El 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)

CampoTipoObligatorioDescripción
call_uuidstringsí
reasonnot_armed · agent_stopped · upload_failedsí
notestringno

Respuestas

200 ​

El hueco queda registrado (el primero cuando se repite).

CampoTipoObligatorioDescripción
gapobjectsí
gap.call_uuidstringsí
gap.reasonnot_armed · agent_stopped · upload_failedsí
gap.sourceagentsí

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-marks

El 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

NombreEnTipoObligatorioDescripción
idpathstring (uuid)síEl id de la grabación que el cliente eligió para la subida de fragmentos.

Cuerpo de la petición (JSON, obligatorio)

CampoTipoObligatorioDescripción
marksarreglo de objectno
marks[].start_msintegersí
marks[].end_msintegersí
marks[].zoneconsumer_card · full_number · payment · focus_lostsí
covered_through_msintegersí

Respuestas

200 ​

Las marcas están registradas y la cobertura está vigente.

CampoTipoObligatorioDescripción
pii_marksobjectsí
pii_marks.recording_idstring (uuid)sí
pii_marks.storedintegersíMarcas escritas por esta solicitud.
pii_marks.duplicatesintegersíMarcas ya registradas.
pii_marks.covered_through_msintegersí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/playback

Requiere 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)

CampoTipoObligatorioDescripción
call_uuidstringsí

Respuestas

200 ​

El acceso queda registrado y la URL está lista.

CampoTipoObligatorioDescripción
playbackobjectsí
playback.recording_idstring (uuid)sí
playback.call_uuidstringsí
playback.variantredactedsíQué video sirve la URL.
playback.urlstringsíRuta bajo el origen de la API: /v1/screen-media/{token}.
playback.expires_atstring (date-time)sí
playback.retained_untilstring (date)sí
playback.chunksintegersí
playback.content_typevideo/webm · video/mp4sí
playback.gapsarreglo de objectsí
playback.gaps[].reasonnot_armed · agent_stopped · upload_failedsí
playback.gaps[].sourceserver · agentsí
playback.gaps[].notestring,nullsí

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-playback

El 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)

CampoTipoObligatorioDescripción
call_uuidstringsí
reasonstringsíPor qué se necesita el original; se almacena junto con el acceso.

Respuestas

200 ​

El acceso queda registrado y la URL está lista.

CampoTipoObligatorioDescripción
playbackobjectsí
playback.recording_idstring (uuid)sí
playback.call_uuidstringsí
playback.variantoriginalsí
playback.urlstringsíRuta bajo el origen de la API: /v1/screen-media/{token}.
playback.expires_atstring (date-time)sí
playback.retained_untilstring (date)sí
playback.chunksintegersí
playback.content_typevideo/webm · video/mp4sí
playback.gapsarreglo de objectsí
playback.gaps[].reasonnot_armed · agent_stopped · upload_failedsí
playback.gaps[].sourceserver · agentsí
playback.gaps[].notestring,nullsí

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/recordings

Metadatos 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

NombreEnTipoObligatorioDescripción
fromquerystring (date-time)noLímite inferior RFC 3339 (campo específico del endpoint; por defecto los últimos 30 días UTC donde se indica).
toquerystring (date-time)noLímite superior RFC 3339.
agent_idquerystring (uuid)no
campaign_idquerystring (uuid)no
dispositionquerystringnoCoincidencia 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.
qquerystringnoBúsqueda de texto libre sobre el número de teléfono enmascarado, account_ref y cualquier referencia externa.
limitqueryintegernoTamaño de página (por defecto 100, máximo 1000).
cursorquerystringnoCursor keyset opaco del next_cursor de la página anterior.

Respuestas

200 ​

Una página de grabaciones.

CampoTipoObligatorioDescripción
recordingsarreglo de RecordingListItemsí
recordings[].idstring (uuid)sí
recordings[].call_idstringsíEl uuid de FreeSWITCH de la llamada (= call_attempts.call_uuid).
recordings[].started_atstring (date-time)síUTC.
recordings[].local_tzstring | nullnoLa zona IANA del destinatario (p. ej. America/Chicago); la interfaz muestra la hora local a partir de ella.
recordings[].agentobject | nullnoDesnormalizado para mostrar; null para llamadas de IA o entrantes sin atender.
recordings[].account_refstring | nullnoReferencia externa de la deuda / número de cuenta.
recordings[].phone_numberstringsíEl E.164 de la otra parte.
recordings[].duration_secsinteger | nullno
recordings[].dispositionstring | nullnoLa 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_statestring | nullnoSe 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_abooleannoLa pierna saliente llevó atestación STIR/SHAKEN A.
recordings[].ai_usedbooleansí
recordings[].retained_untilstring (date)noFecha mínima de retención, congelada en la creación (doctrina de instantánea).
recordings[].legal_holdbooleansí
recordings[].statuspending · available · purged · lostnoDó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_atstring (date-time) | nullnoCuándo se cerró la fila como lost. Se conserva si el audio llegó tarde y la fila volvió a available.
recordings[].lost_reasonno_upload_after_call_end · no_upload_call_never_ended | nullnoPor qué se cerró como lost; null cuando nunca se cerró así.
recordings[].composed_from_partsbooleannotrue 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_msinteger (int64) | nullnoEl 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_cursorstring | nullsí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-exports

Reú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)

CampoTipoObligatorioDescripción
phonestringnoNúmero E.164.
consumer_refstringno
include_piibooleannoExportació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.

CampoTipoObligatorioDescripción
subject_exportobject (SubjectExport)sí
subject_export.idstring (uuid)síEl evento requested de la pista de auditoría.
subject_export.statusdonesí
subject_export.include_piibooleansí
subject_export.urlstring,nullnoURL firmada de corta duración del ZIP.
subject_export.url_expires_atstring,null (date-time)no
subject_export.manifestobjectsíMismo contenido que manifest.json dentro del ZIP.
subject_export.manifest.subjectobjectsí
subject_export.manifest.subject.kindphone · consumer_refno
subject_export.manifest.subject.hashstringnoHMAC del sujeto, en hexadecimal.
subject_export.manifest.include_piibooleansí
subject_export.manifest.generated_atstring (date-time)no
subject_export.manifest.row_cap_per_categoryintegerno
subject_export.manifest.categoriesobjectsí
subject_export.manifest.truncatedarreglo de stringsí
subject_export.manifest.omittedarreglo de objectsí
subject_export.manifest.omitted[].itemstringno
subject_export.manifest.omitted[].reasonstringno

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-requests

Para 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)

CampoTipoObligatorioDescripción
phonestringnoNúmero E.164.
consumer_refstringno
dry_runbooleannoSolo true está disponible en esta versión.
json
{
  "phone": "+13055550123",
  "dry_run": true
}

Respuestas

202 ​

El informe de la simulación (dry run).

CampoTipoObligatorioDescripción
erasure_requestobject (ErasureRequest)sí
erasure_request.idstring (uuid)síLa fila privacy_requests.
erasure_request.dry_runbooleansí
erasure_request.reportobjectsí
erasure_request.report.subjectobjectsí
erasure_request.report.subject.kindphone · consumer_refno
erasure_request.report.subject.hashstringnoHMAC del sujeto, en hexadecimal.
erasure_request.report.dry_runbooleansí
erasure_request.report.generated_atstring (date-time)no
erasure_request.report.records_floor_monthsintegersí
erasure_request.report.floor_basisdeclared · default_max_of_supported_typesno
erasure_request.report.categoriesarreglo de objectsí
erasure_request.report.categories[].categorystringno
erasure_request.report.categories[].tablesarreglo de stringno
erasure_request.report.categories[].foundintegerno
erasure_request.report.categories[].would_anonymizeintegerno
erasure_request.report.categories[].actionanonymize · noneno
erasure_request.report.categories[].eventsintegernoSolo conversaciones de IA: sus eventos.
erasure_request.report.categories[].retainedarreglo de objectno
erasure_request.report.derived_copiesarreglo de objectsí
erasure_request.report.derived_copies[].itemstringno
erasure_request.report.derived_copies[].statusstringno
erasure_request.report.derived_copies[].untilstring,nullno
erasure_request.report.derived_copies[].reasonstringno
erasure_request.report.executionnot_availablesí

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-exports

Encola 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)

CampoTipoObligatorioDescripción
fromstring (date-time)no
tostring (date-time)no
agent_idstring (uuid)no
campaign_idstring (uuid)no
dispositionstringno
qstringno
refreshbooleannoOmite la ventana de deduplicación de corta duración y fuerza una exportación nueva aunque acabe de producirse una idéntica.
include_piibooleannotrue 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.

CampoTipoObligatorioDescripción
exportobject (RecordingExportJob)sí
export.job_idstring (uuid)sí
export.statusqueued · running · done · failed · expiredsí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.piiredacted · rawnoIndica 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.urlstring | nullnoURL firmada del archivo; presente solo cuando status es done.
export.expires_atstring (date-time) | nullno
export.row_countinteger | nullno
export.errorstring | nullno
export.created_atstring (date-time)no
export.updated_atstring (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

NombreEnTipoObligatorioDescripción
job_idpathstring (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.

CampoTipoObligatorioDescripción
exportobject (RecordingExportJob)sí
export.job_idstring (uuid)sí
export.statusqueued · running · done · failed · expiredsí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.piiredacted · rawnoIndica 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.urlstring | nullnoURL firmada del archivo; presente solo cuando status es done.
export.expires_atstring (date-time) | nullno
export.row_countinteger | nullno
export.errorstring | nullno
export.created_atstring (date-time)no
export.updated_atstring (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

NombreEnTipoObligatorioDescripción
idpathstring (uuid)síUUID del recurso. Los ids mal formados o de otro tenant se leen como 404.

Respuestas

200 ​

La grabación.

CampoTipoObligatorioDescripción
recordinganysí

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-url

Devuelve 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

NombreEnTipoObligatorioDescripción
idpathstring (uuid)síUUID del recurso. Los ids mal formados o de otro tenant se leen como 404.

Respuestas

200 ​

Una URL recién firmada.

CampoTipoObligatorioDescripción
urlstringsíURL firmada de corta duración del almacén de objetos.
expires_atstring (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-url

La 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

NombreEnTipoObligatorioDescripción
idpathstring (uuid)síUUID del recurso. Los ids mal formados o de otro tenant se leen como 404.

Cuerpo de la petición (JSON, obligatorio)

CampoTipoObligatorioDescripción
reasonstringsí
json
{
  "reason": "Dispute 2026-0417: customer denies the payment promise"
}

Respuestas

200 ​

Una URL recién firmada del audio original.

CampoTipoObligatorioDescripción
urlstringsíURL firmada de corta duración del almacén de objetos.
expires_atstring (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}/redaction

Tramos (solo tiempo y categoría, nunca el valor), método y versión. 404 cuando la grabación no tiene redacción.

Parámetros

NombreEnTipoObligatorioDescripción
idpathstring (uuid)síUUID del recurso. Los ids mal formados o de otro tenant se leen como 404.

Respuestas

200 ​

La redacción.

CampoTipoObligatorioDescripción
redactionobject (RecordingRedaction)no
redaction.recording_idstring (uuid)no
redaction.methodsilence · beepno
redaction.segmentsarreglo de RecordingRedactionSegmentno
redaction.segments[].start_msintegersí
redaction.segments[].end_msintegersí
redaction.segments[].categorycard · ssn · bank_account · pinsí
redaction.redacted_msintegerno
redaction.versionintegerno
redaction.codecstringno
redaction.requested_bystringno
redaction.created_atstring (date-time)no
redaction.updated_atstring (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}/redaction

Mismo 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

NombreEnTipoObligatorioDescripción
idpathstring (uuid)síUUID del recurso. Los ids mal formados o de otro tenant se leen como 404.

Cuerpo de la petición (JSON, obligatorio)

CampoTipoObligatorioDescripción
methodsilence · beepno
segmentsarreglo de RecordingRedactionSegmentsí
segments[].start_msintegersí
segments[].end_msintegersí
segments[].categorycard · ssn · bank_account · pinsí

Respuestas

200 ​

La redacción después de la llamada.

CampoTipoObligatorioDescripción
redactionobject (RecordingRedaction)no
redaction.recording_idstring (uuid)no
redaction.methodsilence · beepno
redaction.segmentsarreglo de RecordingRedactionSegmentno
redaction.segments[].start_msintegersí
redaction.segments[].end_msintegersí
redaction.segments[].categorycard · ssn · bank_account · pinsí
redaction.redacted_msintegerno
redaction.versionintegerno
redaction.codecstringno
redaction.requested_bystringno
redaction.created_atstring (date-time)no
redaction.updated_atstring (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}/transcript

La 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

NombreEnTipoObligatorioDescripción
idpathstring (uuid)síUUID del recurso. Los ids mal formados o de otro tenant se leen como 404.

Respuestas

200 ​

La transcripción.

CampoTipoObligatorioDescripción
transcriptobject (RecordingTranscript)sí
transcript.recording_idstring (uuid)sí
transcript.redactedbooleansítrue para la vista por defecto (PII redactada); false solo en el original auditado.
transcript.providerstringsíProveedor de voz a texto que la produjo.
transcript.modelstringsí
transcript.languagestring,nullnoIdioma que informó el proveedor, cuando informó alguno.
transcript.audio_secondsintegersíSegundos de audio facturados.
transcript.segment_countintegersí
transcript.created_atstring (date-time)sí
transcript.segmentsarreglo de objectsí
transcript.segments[].seqintegersí
transcript.segments[].channelintegersíCanal de audio: 0 o 1.
transcript.segments[].start_msintegersí
transcript.segments[].end_msintegersí
transcript.segments[].textstringsí

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/original

La 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

NombreEnTipoObligatorioDescripción
idpathstring (uuid)síUUID del recurso. Los ids mal formados o de otro tenant se leen como 404.

Cuerpo de la petición (JSON, obligatorio)

CampoTipoObligatorioDescripción
reasonstringsí
json
{
  "reason": "Dispute 2026-0417: customer denies the payment promise"
}

Respuestas

200 ​

La transcripción, con redacted: false.

CampoTipoObligatorioDescripción
transcriptobject (RecordingTranscript)sí
transcript.recording_idstring (uuid)sí
transcript.redactedbooleansítrue para la vista por defecto (PII redactada); false solo en el original auditado.
transcript.providerstringsíProveedor de voz a texto que la produjo.
transcript.modelstringsí
transcript.languagestring,nullnoIdioma que informó el proveedor, cuando informó alguno.
transcript.audio_secondsintegersíSegundos de audio facturados.
transcript.segment_countintegersí
transcript.created_atstring (date-time)sí
transcript.segmentsarreglo de objectsí
transcript.segments[].seqintegersí
transcript.segments[].channelintegersíCanal de audio: 0 o 1.
transcript.segments[].start_msintegersí
transcript.segments[].end_msintegersí
transcript.segments[].textstringsí

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/search

Busca 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

NombreEnTipoObligatorioDescripción
qquerystringsíLa palabra o frase (de 1 a 8 palabras).
fromquerystring (date-time)síCota inferior RFC 3339 del inicio de la grabación (inclusiva).
toquerystring (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_idquerystring (uuid)noSolo grabaciones de este agente.
campaign_idquerystring (uuid)noSolo grabaciones de esta campaña.
limitqueryintegernoTamaño de página (un valor por encima del techo se recorta, no se rechaza).
cursorquerystringnoCursor 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.

CampoTipoObligatorioDescripción
resultsarreglo de objectsí
results[].recording_idstring (uuid)sí
results[].started_atstring (date-time)sí
results[].agent_idstring,null (uuid)sí
results[].campaign_idstring,null (uuid)sí
results[].matchesarreglo de objectsí
results[].matches[].seqintegersí
results[].matches[].channelintegersíCanal de audio: 0 o 1.
results[].matches[].start_msintegersí
results[].matches[].end_msintegersí
results[].matches[].fragmentstringsíEl texto del segmento alrededor de la coincidencia (unos 40 caracteres de contexto a cada lado).
next_cursorstring,nullsíCursor opaco para la página siguiente; null cuando no queda nada más por examinar.
cappedbooleansí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-hold

Reemplazo 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

NombreEnTipoObligatorioDescripción
idpathstring (uuid)síUUID del recurso. Los ids mal formados o de otro tenant se leen como 404.

Cuerpo de la petición (JSON, obligatorio)

CampoTipoObligatorioDescripción
activebooleansí
reasonstringnoOBLIGATORIO (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.

CampoTipoObligatorioDescripción
recordinganysí

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-policy

Respuestas

200 ​

La política efectiva (valores por defecto materializados en la primera lectura).

CampoTipoObligatorioDescripción
recording_policyobject (RecordingPolicy)sí
recording_policy.enabledbooleansíIndica si las llamadas nuevas se graban por defecto.
recording_policy.retention_monthsintegersí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.channelsmono · stereosí
recording_policy.formatwav · opussí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_atstring (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-policy

Reemplazo 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)

CampoTipoObligatorioDescripción
enabledbooleanno
retention_monthsintegernoPor debajo del mínimo efectivo del tenant (39 hasta que declara sus tipos; GET /v1/business-types) → 422 unprocessable.
channelsmono · stereono
formatwav · opusnoCó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.

CampoTipoObligatorioDescripción
recording_policyobject (RecordingPolicy)sí
recording_policy.enabledbooleansíIndica si las llamadas nuevas se graban por defecto.
recording_policy.retention_monthsintegersí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.channelsmono · stereosí
recording_policy.formatwav · opussí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_atstring (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-types

ADR #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.

CampoTipoObligatorioDescripción
business_typesobject (BusinessTypes)sí
business_types.declaredarray,nullsíLos tipos que el tenant declaró, ordenados. null = no declaró nada.
business_types.supportedarreglo de BusinessTypeFloorsíEl vocabulario cerrado que un tenant puede declarar, cada uno con su mínimo de audio y su fuente.
business_types.supported[].typecobranza · telemarketing · servicio_cliente · financiera_valores · hipotecas_servicingsí
business_types.supported[].audio_floor_monthsintegersí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[].sourcestringsíLa fuente legal, tal como la cita ADR #246 (solo normas federales de EE. UU.).
business_types.retention_floorobjectsíEl mínimo de retención de grabaciones vigente para las grabaciones NUEVAS.
business_types.retention_floor.monthsintegersílegal_floor_months + safety_margin_months. Nunca por debajo del margen; nunca 0.
business_types.retention_floor.legal_floor_monthsintegersí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_monthsintegersí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.basisdeclared · undeclared_maximumsí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_byarreglo de BusinessTypeFloorsí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[].typecobranza · telemarketing · servicio_cliente · financiera_valores · hipotecas_servicingsí
business_types.retention_floor.imposed_by[].audio_floor_monthsintegersí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[].sourcestringsí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-types

Reemplaza 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)

CampoTipoObligatorioDescripción
business_typesarray,nullsí
json
{
  "business_types": [
    "cobranza"
  ]
}

Respuestas

200 ​

La declaración vigente ahora y lo que hizo el cambio.

CampoTipoObligatorioDescripción
business_typesobject (BusinessTypes)sí
business_types.declaredarray,nullsíLos tipos que el tenant declaró, ordenados. null = no declaró nada.
business_types.supportedarreglo de BusinessTypeFloorsíEl vocabulario cerrado que un tenant puede declarar, cada uno con su mínimo de audio y su fuente.
business_types.supported[].typecobranza · telemarketing · servicio_cliente · financiera_valores · hipotecas_servicingsí
business_types.supported[].audio_floor_monthsintegersí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[].sourcestringsíLa fuente legal, tal como la cita ADR #246 (solo normas federales de EE. UU.).
business_types.retention_floorobjectsíEl mínimo de retención de grabaciones vigente para las grabaciones NUEVAS.
business_types.retention_floor.monthsintegersílegal_floor_months + safety_margin_months. Nunca por debajo del margen; nunca 0.
business_types.retention_floor.legal_floor_monthsintegersí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_monthsintegersí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.basisdeclared · undeclared_maximumsí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_byarreglo de BusinessTypeFloorsí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[].typecobranza · telemarketing · servicio_cliente · financiera_valores · hipotecas_servicingsí
business_types.retention_floor.imposed_by[].audio_floor_monthsintegersí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[].sourcestringsíLa fuente legal, tal como la cita ADR #246 (solo normas federales de EE. UU.).
changedbooleansífalse cuando el cuerpo declaró lo que ya estaba declarado (sin fila de auditoría).
loweredbooleansítrue cuando el mínimo efectivo bajó; la fila de auditoría nombra a quien lo hizo.
previous_floor_monthsintegersí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.

Nothing in these docs is legal advice — always confirm compliance posture with your own counsel.