Appearance
Deudas y leads
Generado desde
DialerDigital/corecommit0fc18008b3c8— especificaciónopenapi/dialer-v1.yamlv0.134.0. No se edita a mano; consulta Cómo se construye esta referencia. Descargar la especificación OpenAPI.
Traducción en curso
Esta referencia se genera desde la especificación OpenAPI. Las cadenas que todavía no tienen traducción aparecen en inglés.
Importación masiva de leads, detalle de deuda, CDR por deuda, paquete de defensa.
Endpoints
| Método | Ruta | Resumen |
|---|---|---|
GET | /v1/debts | Lista de cuentas + búsqueda (filtros, paginación por keyset) |
POST | /v1/debts/search | Buscar cuentas con el filtro en el cuerpo de la solicitud (teléfono incluido) |
POST | /v1/debts/import | Importación masiva de leads en JSON o CSV (idempotente) |
GET | /v1/debts/{id} | Deuda + contactos |
GET | /v1/debts/{id}/call_attempts | Historial duradero de CDR por deuda |
GET | /v1/debts/{id}/messages | Qué se envió a una deuda y por qué canal |
GET | /v1/debts/{id}/defense-packet | Paquete de defensa ante litigios (paquete de evidencia sellado) |
GET | /v1/debts/{id}/defense-packet/archives | Listar los paquetes de defensa ante litigios archivados de una deuda |
POST | /v1/debts/{id}/defense-packet/archives | Archivar el paquete de defensa ante litigios (almacén WORM + sello de tiempo RFC 3161) |
GET | /v1/debts/{id}/defense-packet/archives/{aid}/attestation | Kit de atestación de un paquete de defensa ante litigios archivado |
Lista de cuentas + búsqueda (filtros, paginación por keyset)
http
GET /v1/debtsUna página de las cuentas del tenant, leída de la vista ancha debts_wide. Con ámbito de tenant: la deuda de otro tenant nunca se devuelve con ningún filtro ni término de búsqueda — un id, un consumer_ref o un teléfono que pertenezca a otro devuelve una página VACÍA, nunca un 403 (no se confirma la existencia).
Los números de teléfono se devuelven ENMASCARADOS (primary_phone_masked, *** + últimos cuatro). Un listado es una superficie de barrido; el E.164 completo de UNA cuenta queda en GET /v1/debts/{id}. account_number no forma parte de esta proyección en absoluto.
No existe el parámetro phone: un número en la cadena de consulta acabaría en los registros de acceso del ingress, por lo que ?phone= se rechaza con un 400 cuyo código es phone_not_in_query. Busca por teléfono con POST /v1/debts/search, que lo recibe en el cuerpo. consumer_ref y external_ref coinciden de forma EXACTA.
dialable_by selecciona las cuentas marcables en ese instante: una deuda que el motor de disposiciones no ha aparcado (next_dialable_at nulo) cuenta como marcable ahora y se incluye.
campaign_id selecciona la POBLACIÓN de la campaña — las cuentas cuyo propio campaign_id es esa campaña, más los miembros de cualquier lista asociada a ella (/v1/debt-lists) — cada cuenta una sola vez. El campo campaign_id de cada fila sigue siendo la campaña propia de la cuenta (su ranura directa).
campaign_id, state, debt_type, us_state, min_amount_cents y max_amount_cents son los criterios de SEGMENTO (ADR #221): un vocabulario cerrado validado en un solo lugar (Dialer.Debts.Segment) y combinados con AND. Un valor fuera del vocabulario es un 400, nunca una página vacía. Los segmentos seleccionan cuentas solo por atributos: phone, consumer_ref, external_ref y debtor_name nunca son criterios de segmento (este listado conserva sus propias búsquedas exactas por los tres primeros; el nombre del deudor no es buscable). Un parámetro de consulta que este endpoint no conoce se ignora, como antes.
include=dialability (ADR #269) añade a cada fila dialability: por qué la cuenta no es marcable y el consentimiento de cada teléfono, evaluado ahora con las MISMAS compuertas que ejecuta el marcador (DebtDialability). Es opcional y cuesta un número acotado de lecturas por PÁGINA, no por fila, por lo que la página se limita a 50: un limit mayor es un 400, nunca un recorte silencioso. Sin include el cuerpo es idéntico byte a byte al de siempre (el campo está ausente, no es null). dialable: true nunca significa "se realizará una llamada": consulta DebtDialability.not_evaluated_gates. Un valor de include desconocido es un 400.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
campaign_id | query | string (uuid) | no | |
state | query | open · in_collection · promise_to_pay · paid · settled · disputed · closed | no | Estado exacto de la deuda. Un valor desconocido es un 400, no una página vacía. |
state[] | query | arreglo de open · in_collection · promise_to_pay · paid · settled · disputed · closed | no | Varios estados de deuda a la vez (state[]=open&state[]=disputed): una cuenta coincide cuando su estado es CUALQUIERA de ellos. Las repeticiones se ignoran; una entrada vacía o desconocida es un 400. |
debt_type | query | other · student_loan | no | Tipo exacto de deuda. Un valor desconocido es un 400, no una página vacía. Forma parte del vocabulario cerrado de segmentos (Dialer.Debts.Segment, ADR #221). |
debt_type[] | query | arreglo de other · student_loan | no | Varios tipos de deuda a la vez (debt_type[]=other&debt_type[]=student_loan); coincide con CUALQUIERA. |
us_state | query | string | no | Estado de EE. UU. del contacto PRINCIPAL de la cuenta: un código postal USPS de dos letras en mayúsculas (código de estado, posesión o militar; Dialer.Compliance.UsStates). Cualquier otra cosa (tx, XX) es un 400, no una página vacía. Una cuenta sin contacto nunca coincide. |
us_state[] | query | arreglo de string | no | Varios estados de EE. UU. a la vez (us_state[]=TX&us_state[]=NY); coincide con CUALQUIERA. |
min_amount_cents | query | integer | no | Cota inferior (inclusiva) del saldo, en centavos. |
max_amount_cents | query | integer | no | Cota superior (inclusiva) del saldo, en centavos. |
dialable_by | query | string (date-time) | no | Instante RFC-3339; devuelve las cuentas marcables en ese instante o antes. |
consumer_ref | query | string | no | Id exacto de consumidor del CMS. |
external_ref | query | string | no | Id exacto de deuda del CMS. |
limit | query | integer | no | Tamaño de página (por defecto 50, máximo 200). Con include=dialability el máximo es 50 y un valor mayor es un 400. |
include | query | arreglo de dialability | no | Proyecciones adicionales que se añaden a cada fila, separadas por comas (include=dialability); include[]=dialability también se acepta. dialability (ADR #269): por qué la cuenta no es marcable y el consentimiento de cada teléfono, evaluado ahora con las compuertas propias del marcador, opcional y limitado a una página de 50. Un valor desconocido es un 400, nunca se ignora. |
cursor | query | string | no | Cursor keyset opaco del next_cursor de la página anterior. |
Respuestas
200
Una página de cuentas.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
debts | arreglo de DebtSummary | sí | |
debts[].id | string (uuid) | no | |
debts[].tenant_id | string (uuid) | no | |
debts[].campaign_id | string (uuid) | null | no | La ranura DIRECTA de la deuda, como en Debt. Una deuda que también está en una campaña por una lista se lista bajo ?campaign_id= de esa campaña con este campo sin cambios. |
debts[].campaign_name | string | null | no | |
debts[].external_ref | string | no | |
debts[].consumer_ref | string | no | |
debts[].debt_type | other · student_loan | no | |
debts[].amount_cents | integer | no | |
debts[].currency | string | no | |
debts[].state | open · in_collection · promise_to_pay · paid · settled · disputed · closed | no | |
debts[].last_conversation_at | string (date-time) | null | no | |
debts[].next_dialable_at | string (date-time) | null | no | Nulo significa que la deuda es marcable ahora (no está aparcada por el motor de disposiciones). |
debts[].primary_phone_masked | string | null | no | *** + los últimos cuatro del contacto principal; null cuando la deuda no tiene contacto. |
debts[].primary_line_type | mobile · landline · voip · unknown | null | no | |
debts[].primary_timezone | string | null | no | |
debts[].primary_us_state | string | null | no | |
debts[].primary_city | string | null | no | |
debts[].contact_count | integer | no | |
debts[].created_at | string (date-time) | no | |
debts[].updated_at | string (date-time) | no | |
debts[].dialability | object (DebtDialability) | no | Presente solo con include=dialability (ADR #269). El veredicto es "ahora" y envejece en segundos: informa, nunca concede permiso; el motor decide en el momento de marcar. Se evalúa con las compuertas PROPIAS del marcador sobre una única lectura de instantánea por página. |
debts[].dialability.evaluated_at | string (date-time) | sí | |
debts[].dialability.dialable | boolean | sí | El teléfono que el marcador llamaría pasa todas las compuertas evaluadas Y no se aplica ningún motivo a nivel de cuenta. NO es una promesa de que se realizará una llamada: consulta not_evaluated_gates. |
debts[].dialability.blocked_reasons | arreglo de DebtBlockedReason | sí | Los motivos de la cuenta más los del teléfono que el marcador llamaría; vacío si y solo si dialable. ["no_phones"] cuando la cuenta no tiene contacto. |
debts[].dialability.eligible_at | string (date-time) | null | sí | evaluated_at cuando nada bloquea. De lo contrario, el primer instante, dentro de 9 días, en que las compuertas dependientes del tiempo (horario de silencio, Reg F, la matriz de estados) dejan de bloquear el teléfono que el marcador llamaría, SOLO cuando nada más lo bloquea; null cuando se aplica otro motivo o no existe tal instante. |
debts[].dialability.phones | arreglo de DebtDialabilityPhone | sí | Hasta 5 contactos, en el orden de rango del marcador. Vacío cuando la cuenta no pudo evaluarse. |
debts[].dialability.phones[].phone_masked | string | null | sí | *** + los últimos cuatro dígitos, nunca más. |
debts[].dialability.phones[].line_type | mobile · landline · voip · unknown | null | sí | |
debts[].dialability.phones[].feed_candidate | boolean | sí | Verdadero para el teléfono que el marcador llamaría (el contacto de rango 1 de la cuenta: primero el principal, luego el más antiguo). Los demás son informativos. |
debts[].dialability.phones[].consent | object (DebtDialabilityConsent) | sí | El consentimiento registrado para el número. Solo el estado, el canal y el instante: nunca la fuente en texto libre. |
debts[].dialability.phones[].dialable | boolean | sí | Todas las compuertas evaluadas dejan pasar ESTE teléfono; blocked_reasons está vacío si y solo si es verdadero. |
debts[].dialability.phones[].blocked_reasons | arreglo de DebtBlockedReason | sí | TODOS los motivos, no solo el primero (las compuertas no se cortocircuitan), en el orden de la cadena de compuertas del marcador. |
debts[].dialability.phones_truncated | boolean | sí | La cuenta tiene más contactos de los que lista phones. |
debts[].dialability.not_evaluated_gates | arreglo de string | sí | Lo que dialable NO cubre porque depende de la campaña, la llamada o el tenant (divulgación de IA, voz artificial, atestación del identificador de llamada, estado de la campaña, plan, topes de gasto, ritmo de marcación). |
next_cursor | string | null | sí | Cursor keyset opaco; null en la última página. |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Buscar cuentas con el filtro en el cuerpo de la solicitud (teléfono incluido)
http
POST /v1/debts/searchMismos filtros, paginación, ámbito de tenant (RLS) y proyección enmascarada que GET /v1/debts, con el filtro en un cuerpo JSON. Existe para que el número de teléfono de un deudor nunca viaje en la URL (registros de acceso del ingress y del proxy). Una búsqueda es una lectura: no cambia ningún estado.
phone busca mediante índice ciego determinista y exige un E.164 normalizado (+13125550100); cualquier otra cosa es un 400, no una página vacía. Una clave del cuerpo fuera del esquema es un 400. Un cuerpo vacío {} devuelve la primera página de toda la cartera.
"include": ["dialability"] añade el veredicto de marcabilidad por cuenta, con el mismo significado, límite (página de 50) y costo que en GET /v1/debts.
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
campaign_id | string (uuid) | no | |
state | open · in_collection · promise_to_pay · paid · settled · disputed · closed | arreglo de open · in_collection · promise_to_pay · paid · settled · disputed · closed | no | Un estado de deuda o varios (coincide con CUALQUIERA). |
debt_type | other · student_loan | arreglo de other · student_loan | no | Un tipo de deuda o varios (coincide con CUALQUIERA). |
us_state | string | arreglo de string | no | Un código USPS de dos letras del contacto PRINCIPAL o varios (coincide con CUALQUIERA). |
min_amount_cents | integer | no | |
max_amount_cents | integer | no | |
dialable_by | string (date-time) | no | |
phone | string | no | E.164 normalizado de cualquier contacto de la deuda. |
consumer_ref | string | no | |
external_ref | string | no | |
limit | integer | no | Tamaño de página. Con include el máximo es 50 y un valor mayor es un 400. |
cursor | string | no | |
include | arreglo de dialability | no | Proyecciones adicionales para cada fila (["dialability"], ADR #269): mismo significado, límite y costo que include en GET /v1/debts. |
Respuestas
200
Una página de cuentas.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
debts | arreglo de DebtSummary | sí | |
debts[].id | string (uuid) | no | |
debts[].tenant_id | string (uuid) | no | |
debts[].campaign_id | string (uuid) | null | no | La ranura DIRECTA de la deuda, como en Debt. Una deuda que también está en una campaña por una lista se lista bajo ?campaign_id= de esa campaña con este campo sin cambios. |
debts[].campaign_name | string | null | no | |
debts[].external_ref | string | no | |
debts[].consumer_ref | string | no | |
debts[].debt_type | other · student_loan | no | |
debts[].amount_cents | integer | no | |
debts[].currency | string | no | |
debts[].state | open · in_collection · promise_to_pay · paid · settled · disputed · closed | no | |
debts[].last_conversation_at | string (date-time) | null | no | |
debts[].next_dialable_at | string (date-time) | null | no | Nulo significa que la deuda es marcable ahora (no está aparcada por el motor de disposiciones). |
debts[].primary_phone_masked | string | null | no | *** + los últimos cuatro del contacto principal; null cuando la deuda no tiene contacto. |
debts[].primary_line_type | mobile · landline · voip · unknown | null | no | |
debts[].primary_timezone | string | null | no | |
debts[].primary_us_state | string | null | no | |
debts[].primary_city | string | null | no | |
debts[].contact_count | integer | no | |
debts[].created_at | string (date-time) | no | |
debts[].updated_at | string (date-time) | no | |
debts[].dialability | object (DebtDialability) | no | Presente solo con include=dialability (ADR #269). El veredicto es "ahora" y envejece en segundos: informa, nunca concede permiso; el motor decide en el momento de marcar. Se evalúa con las compuertas PROPIAS del marcador sobre una única lectura de instantánea por página. |
debts[].dialability.evaluated_at | string (date-time) | sí | |
debts[].dialability.dialable | boolean | sí | El teléfono que el marcador llamaría pasa todas las compuertas evaluadas Y no se aplica ningún motivo a nivel de cuenta. NO es una promesa de que se realizará una llamada: consulta not_evaluated_gates. |
debts[].dialability.blocked_reasons | arreglo de DebtBlockedReason | sí | Los motivos de la cuenta más los del teléfono que el marcador llamaría; vacío si y solo si dialable. ["no_phones"] cuando la cuenta no tiene contacto. |
debts[].dialability.eligible_at | string (date-time) | null | sí | evaluated_at cuando nada bloquea. De lo contrario, el primer instante, dentro de 9 días, en que las compuertas dependientes del tiempo (horario de silencio, Reg F, la matriz de estados) dejan de bloquear el teléfono que el marcador llamaría, SOLO cuando nada más lo bloquea; null cuando se aplica otro motivo o no existe tal instante. |
debts[].dialability.phones | arreglo de DebtDialabilityPhone | sí | Hasta 5 contactos, en el orden de rango del marcador. Vacío cuando la cuenta no pudo evaluarse. |
debts[].dialability.phones[].phone_masked | string | null | sí | *** + los últimos cuatro dígitos, nunca más. |
debts[].dialability.phones[].line_type | mobile · landline · voip · unknown | null | sí | |
debts[].dialability.phones[].feed_candidate | boolean | sí | Verdadero para el teléfono que el marcador llamaría (el contacto de rango 1 de la cuenta: primero el principal, luego el más antiguo). Los demás son informativos. |
debts[].dialability.phones[].consent | object (DebtDialabilityConsent) | sí | El consentimiento registrado para el número. Solo el estado, el canal y el instante: nunca la fuente en texto libre. |
debts[].dialability.phones[].dialable | boolean | sí | Todas las compuertas evaluadas dejan pasar ESTE teléfono; blocked_reasons está vacío si y solo si es verdadero. |
debts[].dialability.phones[].blocked_reasons | arreglo de DebtBlockedReason | sí | TODOS los motivos, no solo el primero (las compuertas no se cortocircuitan), en el orden de la cadena de compuertas del marcador. |
debts[].dialability.phones_truncated | boolean | sí | La cuenta tiene más contactos de los que lista phones. |
debts[].dialability.not_evaluated_gates | arreglo de string | sí | Lo que dialable NO cubre porque depende de la campaña, la llamada o el tenant (divulgación de IA, voz artificial, atestación del identificador de llamada, estado de la campaña, plan, topes de gasto, ritmo de marcación). |
next_cursor | string | null | sí | Cursor keyset opaco; null en la última página. |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Importación masiva de leads en JSON o CSV (idempotente)
http
POST /v1/debts/importAcepta el sobre JSON existente o una carga multipart/form-data. El formulario multipart lleva file (text/csv), mapping (un objeto JSON cuyas claves son destinos de importación y cuyos valores son nombres de cabecera del CSV), y separator opcional (comma, semicolon o tab; por defecto comma). Destinos de deuda: los nombres de campo JSON menos state. Una columna last_conversation_at mapeada pasa por la misma validación que el campo JSON (RFC 3339 con desfase UTC, nunca en el futuro) y siembra la misma conversación cms_import; una celda incorrecta falla solo su fila, y una celda en blanco equivale a omitir el campo. Se puede mapear un único contacto con contact.phone_e164, contact.line_type, contact.timezone, contact.us_state, contact.city, contact.postal_code, contact.is_primary o contact.consumer_ref. El nombre del deudor se mapea a debtor_name (opcional, máximo 200 caracteres, cifrado en reposo; consulta DebtImportItem). Una columna delinquent_since mapeada (una fecha YYYY-MM-DD) pasa por la misma validación que el campo JSON; una celda incorrecta falla solo su fila.
Con ?mapping_id= el formulario multipart lleva solo file: la plantilla almacenada aporta el mapeo y el separador, y enviar también mapping o separator se rechaza con 422. Una plantilla que mapea un mismo campo desde dos columnas, un campo que el importador CSV no conoce, o options.default_currency (la carga no tiene moneda por defecto), responde 422 y no importa nada.
El CSV es UTF-8; se acepta un BOM inicial. El límite de archivo por defecto es 4 000 000 de bytes y el límite de filas por defecto es 10 000 filas de datos. Se gobiernan por tenant mediante las claves del catálogo R19 debt_csv_max_bytes y debt_import_max_rows; esta última también se aplica a JSON. El análisis es fail-closed: un CSV mal formado, una codificación no válida, campos multipart ausentes o desconocidos, cabeceras mapeadas ausentes o un separador no admitido no importan nada y responden 422; un campo demasiado grande responde 413. El archivo cargado permanece en memoria y nunca se escribe en disco ni se registra.
Tras el análisis y el mapeo, ambas representaciones entran en el MISMO pipeline de depuración, normalización e importación por elemento. Tolerancia a fallos por elemento, el límite de filas indicado arriba por solicitud y un máximo de 100 contactos por deuda. Las deudas se actualizan o insertan por external_ref (LA clave del contador de Reg F) y los contactos por phone_e164. Reenviar la misma carga es seguro. Las filas fallidas devuelven solo su índice base cero y el motivo; no se repite ningún valor de celda del CSV.
Un campo no reconocido en un elemento — o en uno de sus contactos — hace fallar ESE elemento y nada más: vuelve en failed con el índice propio del elemento y un mensaje que nombra la clave, mientras todos los demás elementos se importan. Un contacto incorrecto se localiza por su posición dentro del elemento (contact 0: ...). La tolerancia a fallos es la razón por la que esta ruta responde por elemento en lugar de rechazar todo el cuerpo con un 400 como hacen las rutas de objeto único.
Un contacto enviado con is_primary: true afirma cuál es EL principal de esa deuda: el principal anterior se degrada en la misma transacción y se conserva (los números nunca se borran), y el nuevo se inserta o se promueve. Omitir is_primary, o enviar false, nunca cambia el principal existente. Un elemento con más de un is_primary: true falla ESE elemento y no escribe nada — la corrección es ambigua y que gane el último la perdería en silencio.
La importación es ADITIVA sobre los campos de datos de un contacto, y son TRIESTADO. Omitir un campo y enviarlo como "" (o como espacios en blanco) significan lo mismo — "el feed no lo dijo" — y el valor almacenado queda exactamente como estaba; una reimportación puede por tanto CORREGIR un valor pero nunca BORRARLO. Enviar el campo como null tampoco es un borrado: hace FALLAR ese elemento, con un mensaje que nombra el campo, y no se escribe nada para él. No hay ningún verbo en esta ruta que vacíe un campo almacenado, y es deliberado: "" es lo que un exportador pone en cada celda en blanco de un feed masivo, así que leerlo como "borrar" haría que una exportación rutinaria eliminara en silencio el estado y el código postal que deciden el tope por deuda de NYC. line_type: "unknown" NO es un valor vacío — es un miembro publicado del enum, de modo que el feed SÍ está reclasificando el número y se aplica.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
mapping_id | query | string (uuid) | no | La plantilla de mapeo de importación con la que se mapeó el archivo. Se resuelve ANTES de que se importe nada (404 cuando no es del tenant, 409 cuando está archivada: no se escribe nada) y la versión usada se registra después de la importación (GET /v1/import-mappings/{id}/uses). En el formulario multipart de POST /v1/debts/import la plantilla reemplaza las partes mapping y separator. Si se omite, la importación no cambia. |
mapping_version | query | integer | no | La versión de mapping_id que se registra. Por defecto, la actual. Requiere mapping_id. |
import_id | query | string (uuid) | no | Nombra UNA importación que el cliente envía en varias solicitudes (fragmentos). Los usos que esas solicitudes dejan se leen en GET /v1/import-mappings/{id}/uses como un único uso — los conteos sumados, id = este import_id — siempre que compartan la versión y la lista. Requiere mapping_id (400 en caso contrario, y para un valor que no sea un UUID). Si se omite, cada solicitud es su propio uso. |
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
debts | arreglo de DebtImportItem | sí | |
debts[].external_ref | string | sí | LA clave del contador de Reg F (clave de upsert). |
debts[].consumer_ref | string | sí | |
debts[].debt_type | other · student_loan | no | |
debts[].account_number | string | no | |
debts[].debtor_name | string | null | no | Nombre completo opcional del deudor, texto libre. Los espacios en blanco se colapsan y se recortan; un valor en blanco se almacena como sin nombre. Más de 200 caracteres hace fallar el ELEMENTO (error de fila debtor_name should be at most 200 character(s)), no el lote. Cifrado en reposo, no buscable, y nunca se envía a analítica, webhooks ni registros. Como los demás campos de la deuda es una instantánea: una reimportación que lo omite borra el nombre almacenado. |
debts[].amount_cents | integer | no | |
debts[].currency | string | no | |
debts[].state | open · in_collection · promise_to_pay · paid · settled · disputed · closed | no | |
debts[].campaign_id | string (uuid) | no | |
debts[].prior_attempts_7d | integer | no | Cuántas llamadas sobre ESTA deuda ya realizó el cliente desde su sistema anterior en los últimos 7 días. Sin él, el contador 7-en-7 de Reg F arranca A CIEGAS en una cartera migrada — solo vio los intentos que realizó esta plataforma — y una cartera trasladada a mitad de ciclo puede agotar el tope legal en su primera semana. Se materializa como filas de intento que la compuerta ya lee, de modo que la compuerta en sí no cambia. Es un TOTAL para la ventana, no un delta: reenviar la misma carga no suma nada, y reenviar un número MENOR nunca retracta el historial (el conteo solo sube, como last_conversation_at). Los valores superiores a 7 se almacenan como 7 — 7 ya satura todos los topes de frecuencia que tiene el motor, así que el veredicto es idéntico y el tope solo acota la escritura. Un valor que no sea un entero >= 0 hace fallar ESE elemento, como cualquier otro campo incorrecto en esta ruta; omitirlo no cambia nada. |
debts[].last_conversation_at | string (date-time) | no | Cuándo habló el cliente por última vez con el deudor, según su sistema anterior (RFC 3339 con desfase UTC). Sin él, el periodo de enfriamiento de 7 días posterior al contacto de Reg F (12 CFR 1006.14(b)(2)(i)(B)) arranca A CIEGAS en una cartera migrada: la plataforma solo conoce las conversaciones que registró y podría volver a llamar dentro de la ventana. Se materializa como una fila de conversations con source: cms_import, que la compuerta ya lee, de modo que la compuerta en sí no cambia; el ancla last_conversation_at de la deuda avanza hasta ella. Reenviar la misma carga no suma nada, y una fecha MÁS ANTIGUA nunca retrocede el ancla. Los reportes que miden la actividad de la plataforma (penetración, tasa de abandono, la exportación de intentos) no la cuentan. Un valor mal formado, uno sin desfase UTC o uno en el futuro hace fallar ESE elemento (una fecha futura mantendría abierto el enfriamiento más allá de su ventana); omitirlo no cambia nada. |
debts[].delinquent_since | string (date) | no | La fecha de calendario en que la deuda entró en mora (YYYY-MM-DD), según el sistema anterior del cliente. Es la antigüedad de la deuda: los filtros de segmentación como "en mora de 31 a 90 días" se calculan contra ella. Es una instantánea, como debtor_name: reimportar la deuda sin ella la borra. Una deuda sin fecha nunca coincide con un filtro de antigüedad. Cualquier cosa que no sea una fecha de calendario real en ese formato exacto (null, una cadena vacía, un número, una marca de tiempo, 2026-02-30) o una fecha futura hace fallar ESE elemento; omitirla está bien. En una importación CSV una celda en blanco equivale a omitir el campo. |
debts[].contacts | arreglo de object | no | |
debts[].contacts[].phone_e164 | string | sí | E.164 (clave de upsert dentro de la deuda). |
debts[].contacts[].line_type | mobile · landline · voip · unknown | no | |
debts[].contacts[].timezone | string | no | IANA. |
debts[].contacts[].us_state | string | no | |
debts[].contacts[].city | string | no | |
debts[].contacts[].postal_code | string | no | Código postal del consumidor. Es lo que resuelve el distrito de NYC para el tope por deuda: sin él, un contacto de NY no puede descartarse de la ciudad y recibe el tope estricto. Se actualiza en la reimportación solo cuando la carga lo incluye — omitirlo, o enviarlo como "", nunca borra un valor almacenado, y enviarlo como null hace fallar el elemento. Misma regla que consumer_ref, timezone, us_state y city: el contrato es uno solo, aunque esta columna sea la única anulable de las seis. |
debts[].contacts[].is_primary | boolean | no | Afirma EL contacto principal de la deuda. En la importación, true degrada al principal anterior (se conserva, nunca se borra) y promueve a este; omitido o false deja intacto al principal existente. Más de uno por elemento hace fallar el elemento. |
debts[].contacts[].consumer_ref | string | no |
json
{
"debts": [
{
"external_ref": "cms-debt-0001",
"consumer_ref": "cms-consumer-9001",
"debt_type": "other",
"account_number": "ACC-1001",
"debtor_name": "Ana María Pérez",
"amount_cents": 125000,
"currency": "USD",
"campaign_id": "3f2c1b0a-9d8e-4c7b-a6f5-4e3d2c1b0a99",
"contacts": [
{
"phone_e164": "+13125551001",
"line_type": "mobile",
"timezone": "America/Chicago",
"us_state": "IL",
"city": "Chicago",
"is_primary": true
}
]
}
]
}Respuestas
200
Resultado de la importación (tolerancia a fallos por elemento).
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
imported | integer | sí | |
failed | arreglo de object | sí | |
failed[].index | integer | no | |
failed[].error | string | no | |
mapping | object | no | Presente solo cuando la solicitud nombró mapping_id: la versión de la plantilla registrada como usada. |
mapping.id | string (uuid) | sí | |
mapping.version | integer | sí |
json
{
"imported": 1,
"failed": []
}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"
}
}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"
}
}413
El archivo CSV u otro campo multipart supera su límite de bytes.
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 — ImportThrottled
La importación se rechazó ANTES de leer ninguna fila (core#2505). Dos códigos, ambos con Retry-After. import_in_progress: el tenant ya tiene en ejecución su máximo de importaciones (2 por defecto, por nodo); reintenta cuando termine una. rate_limited: más de 10 importaciones en la ventana fija (por defecto) para el tenant, que es un límite por tenant y no por clave porque el pool de conexiones que drena una importación es el de la celda. Se aplica por igual a POST /v1/debts/import y POST /v1/debt-lists/{id}/import. La cuota de solicitudes por clave aún puede responder rate_limited primero.
in_flight
json
{
"error": {
"code": "import_in_progress",
"message": "an import of this tenant is still running; retry when it finishes"
}
}window
json
{
"error": {
"code": "rate_limited",
"message": "import rate limit exceeded; retry later"
}
}Deuda + contactos
http
GET /v1/debts/{id}Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string (uuid) | sí | UUID del recurso. Los ids mal formados o de otro tenant se leen como 404. |
Respuestas
200
La deuda con sus contactos.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
debt | object (Debt) | sí | |
debt.id | string (uuid) | no | |
debt.tenant_id | string (uuid) | no | |
debt.campaign_id | string (uuid) | null | no | La ranura DIRECTA de la deuda: la campaña a la que se importó o asignó. No es la respuesta completa a "qué campañas trabajan esta deuda": una deuda también está en la población de toda campaña a la que esté asociada una lista que la contenga (/v1/debt-lists). |
debt.external_ref | string | no | |
debt.consumer_ref | string | no | |
debt.debt_type | other · student_loan | no | |
debt.account_number | string | no | |
debt.debtor_name | string | null | no | El nombre del deudor tal como lo envió el CMS en la importación (core#1529); null cuando no envió ninguno. Cifrado en reposo. Lo sirven GET /v1/debts/{id}, la tarjeta del deudor y la tarjeta de llamada del agente; NO lo sirven el listado GET /v1/debts, los webhooks ni la exportación de analítica. |
debt.amount_cents | integer | no | |
debt.currency | string | no | |
debt.state | open · in_collection · promise_to_pay · paid · settled · disputed · closed | no | |
debt.last_conversation_at | string (date-time) | null | no | |
debt.contacts | arreglo de Contact | no | Presente solo en GET /v1/debts/{id}. |
debt.contacts[].id | string (uuid) | no | |
debt.contacts[].debt_id | string (uuid) | no | |
debt.contacts[].consumer_ref | string | no | |
debt.contacts[].phone_e164 | string | no | |
debt.contacts[].line_type | mobile · landline · voip · unknown | no | |
debt.contacts[].timezone | string | null | no | |
debt.contacts[].us_state | string | null | no | |
debt.contacts[].city | string | null | no | |
debt.contacts[].postal_code | string | null | no | |
debt.contacts[].is_primary | boolean | no | |
debt.created_at | string (date-time) | no | |
debt.updated_at | string (date-time) | no |
401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}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"
}
}Historial duradero de CDR por deuda
http
GET /v1/debts/{id}/call_attemptsHistorial duradero completo de la deuda (originales + correcciones compensatorias), paginado por keyset. Acepta los mismos filtros que GET /v1/call_attempts; el debt_id de la ruta siempre prevalece sobre cualquier ?debt_id= de la consulta.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string (uuid) | sí | UUID del recurso. Los ids mal formados o de otro tenant se leen como 404. |
campaign_id | query | string (uuid) | no | |
agent_id | query | string (uuid) | no | |
disposition | query | string | no | Coincidencia exacta contra un vocabulario ABIERTO: call_attempts.disposition se escribe TEXTUALMENTE (20260725000003_create_disposition_engine.exs) y los valores del motor — answered_human, abandoned, delivered, ai_conversation, answered_machine, ai_stream_lost, busy, no_answer, canceled, rejected, failed — son un SUBCONJUNTO NOMBRADO de él, no su frontera: los resultados tipificados del agente o de la IA (promise_to_pay, wrong_number) y los marcadores de plataforma (not_placed) son igualmente válidos. Un código desconocido responde 200 con cero filas, nunca 400. Lo que SÍ es 400 es una FORMA mal formada: la cadena vacía, y el parámetro repetido o enviado en forma de arreglo. |
from | query | string (date-time) | no | Límite inferior RFC 3339 (campo específico del endpoint; por defecto los últimos 30 días UTC donde se indica). |
to | query | string (date-time) | no | Límite superior RFC 3339. |
limit | query | integer | no | Tamaño de página (por defecto 100, máximo 1000). |
cursor | query | string | no | Cursor keyset opaco del next_cursor de la página anterior. |
Respuestas
200
Una página de filas de CDR.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
call_attempts | arreglo de CallAttempt | sí | |
call_attempts[].id | string (uuid) | no | |
call_attempts[].tenant_id | string (uuid) | no | |
call_attempts[].debt_id | string (uuid) | no | |
call_attempts[].campaign_id | string (uuid) | null | no | |
call_attempts[].agent_id | string (uuid) | null | no | |
call_attempts[].consumer_ref | string | no | |
call_attempts[].debt_key | string | no | |
call_attempts[].call_uuid | string | null | no | |
call_attempts[].from_number | string | null | no | |
call_attempts[].to_number | string | no | |
call_attempts[].started_at | string (date-time) | null | no | |
call_attempts[].answered_at | string (date-time) | null | no | |
call_attempts[].ended_at | string (date-time) | null | no | |
call_attempts[].disposition | string | null | no | Disposiciones del motor: answered_human|abandoned|delivered|ai_conversation|answered_machine|ai_stream_lost|busy|no_answer|canceled|rejected|failed; los resultados tipificados del agente o de la IA son de forma libre (p. ej. promise_to_pay). Una llamada contestada que nunca se puentió se nombra por lo que fue la respuesta (core#1323): answered_machine ante un veredicto de máquina de AMD, ai_conversation para una campaña de IA de voz, delivered para una campaña de TTS/IVR/encuesta, y abandoned SOLO cuando se esperaba un agente (la llamada abandonada de TSR/FCC). Las filas escritas antes de ese release conservan abandoned para las cuatro. Una llamada de IA de voz cuya sesión la IA de voz rechazó por estar llena, y que ningún agente atendió, también es abandoned (core#1326): la persona contestó y nadie le habló. |
call_attempts[].hangup_cause | string | null | no | |
call_attempts[].sip_response_code | integer | null | no | |
call_attempts[].compliance_snapshot | object | null | no | Evidencia congelada de la decisión en el momento de originar. |
call_attempts[].amd_verdict | string | null | no | |
call_attempts[].recording_uri | string | null | no | Dónde quedó la grabación de la llamada (ruta u URI del objeto); null cuando la llamada no se grabó. |
call_attempts[].ai_used | boolean | no | Si el bot de IA de voz se ejecutó en esta llamada (la clave de medición del complemento de IA). |
call_attempts[].note | string | null | no | La nota de texto libre del agente sobre la llamada, introducida al asignar la disposición en CUALQUIER disposición. Viaja en el registro de corrección que lleva la disposición, por lo que la fila ORIGINAL normalmente es null. Cifrada en reposo (sobre por tenant) y nunca consultada por contenido. null = sin nota, o un registro escrito antes de que existiera la columna. |
call_attempts[].corrects_id | string (uuid) | null | no | |
call_attempts[].inserted_at | string (date-time) | no | |
call_attempts[].disclosure | object (CallAttemptDisclosure) | no | Lo que el agente DECLARÓ sobre el aviso obligatorio de cobranza en esta llamada (core#1587, ADR #211). En GET /v1/call_attempts y en las filas del paquete de defensa v2; todas las filas de un intento (la original y sus correcciones) llevan el estado del intento. Es una declaración, nunca una prueba de que el aviso se dijo o se oyó: no existe un estado "verificado" ni "conforme". attested — la atestación vigente dice delivered. declared_not_delivered — dice not_delivered (el motivo está en el evento). unattested — la llamada ESTÁ en alcance (tiene deuda y el pipeline la finalizó como answered_human o transferred) y nadie atestó. not_applicable — fuera de alcance (sin deuda, o no es un contacto humano). undecidable — la llamada aún no tiene fila final, así que el alcance no puede decidirse. El alcance nunca depende del código que tecleó el agente. |
call_attempts[].disclosure.status | attested · declared_not_delivered · unattested · not_applicable · undecidable | sí | |
call_attempts[].disclosure.event_id | string (uuid) | null | sí | La atestación vigente (null cuando no hay ninguna). |
call_attempts[].disclosure.at | string (date-time) | null | sí | Hora del servidor de la atestación vigente. |
call_attempts[].disclosure.revised | boolean | sí | true cuando el intento tiene más de una atestación (una rectifica a otra). |
call_attempts[].disclosure.script | CallAttemptDisclosureScript | null | no | Solo en GET /v1/call_attempts (core#1690, ADR #219 Decisiones 3-5); las filas del paquete de defensa v2 no la incluyen. El guion aprobado que se aplica a esta llamada, resuelto en el servidor a partir de los hechos congelados en el intento (estado del consumidor y tipo de deuda), de modo que la estación no calcula nada. Solo participan las versiones aprobadas antes de que terminara la llamada (o aprobadas ahora, mientras no ha terminado) y no retiradas hoy, así que la versión mostrada es una que la atestación acepta. null cuando el tenant no tiene tal versión, cuando el intento no tiene hechos congelados, o cuando ninguna versión aplica. No es la versión que el agente atestó: esa está sellada en el evento de atestación. |
call_attempts[].disclosure.script_match | matched · other_version · unscripted · not_applicable | no | Sólo en GET /v1/call_attempts (core#1691, ADR #219 Decision 7). Compara la versión del guion que NOMBRA la atestación vigente con la que script publica ahora. matched: la misma. other_version: nombra otra (por ejemplo una sustituida después de iniciada la llamada, que sigue siendo válida) o ya no hay ninguna publicada. unscripted: hay una atestación que no nombra ninguna versión y sí se aplicó un guion. not_applicable: no hay atestación, o no hay guion con el que comparar. Es una marca de lectura: no bloquea nada y no afirma que ningún texto cumpla ninguna norma. |
next_cursor | string | null | sí | Cursor keyset opaco; null en la última página. |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}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"
}
}Qué se envió a una deuda y por qué canal
http
GET /v1/debts/{id}/messagesLectura unificada por deuda sobre los libros de SMS y de correo electrónico (core#1772). Es un modelo de lectura DERIVADO en el momento de leer (ADR #218): sin vista materializada. Cada entrada es un ORIGINAL saliente; status es el vigente (la última transición). Con ventana (from/to, máximo 31 días, por defecto los últimos 31) y paginación por keyset (por defecto 50, máximo 200); channel limita a un solo libro.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string (uuid) | sí | UUID del recurso. Los ids mal formados o de otro tenant se leen como 404. |
from | query | string (date-time) | no | Límite inferior RFC 3339 (campo específico del endpoint; por defecto los últimos 30 días UTC donde se indica). |
to | query | string (date-time) | no | Límite superior RFC 3339. |
channel | query | sms · email | no | |
limit | query | integer | no | Tamaño de página (por defecto 100, máximo 1000). |
cursor | query | string | no | Cursor keyset opaco del next_cursor de la página anterior. |
Respuestas
200
Una página de mensajes enviados, del más reciente al más antiguo.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
messages | arreglo de DebtMessage | no | |
messages[].channel | sms · email | no | |
messages[].id | string (uuid) | no | |
messages[].status | string | no | Estado vigente (la última transición del original). |
messages[].recipient | string | null | no | |
messages[].subject | string | null | no | |
messages[].body_preview | string | null | no | |
messages[].provider_id | string (uuid) | null | no | |
messages[].campaign_id | string (uuid) | null | no | |
messages[].debt_id | string (uuid) | no | |
messages[].occurred_at | string (date-time) | no | |
messages[].last_event_at | string (date-time) | no | |
next_cursor | string | null | 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"
}
}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"
}
}Paquete de defensa ante litigios (paquete de evidencia sellado)
http
GET /v1/debts/{id}/defense-packetUN solo paquete de evidencia JSON sellado por deuda, ensamblado en una única transacción del tenant: historial completo e inmutable de CDR con instantáneas de cumplimiento congeladas, rechazos de compuertas, estado de consentimiento/DNC/cese y desistimiento (C&D), promesas, rellamadas, auditoría de supervisión y las versiones de política vigentes. integrity.digest es un SHA-256 sobre el JSON canónico SIN la clave integrity (claves de objeto ordenadas bytewise, sin espacios no significativos: cualquier tercero puede reverificarlo; receta en API.md). Con ?format=zip (o Accept: application/zip) entrega un ZIP con packet.json + summary.txt con Content-Disposition: attachment y X-Artifact-SHA256 calculado sobre los bytes del ZIP.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string (uuid) | sí | UUID del recurso. Los ids mal formados o de otro tenant se leen como 404. |
format | query | zip | no | zip para el artefacto descargable (equivalente: Accept: application/zip). |
Respuestas
200
El paquete de evidencia sellado (sobre JSON o artefacto ZIP).
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
packet | "litigation_defense" | no | |
packet_version | integer | no | |
tenant_id | string (uuid) | no | |
packet_generated_at | string (date-time) | no | |
debt | object | no | |
contacts | arreglo de object | no | |
call_attempts | arreglo de object | no | Historial COMPLETO, originales + correcciones, cada uno con su compliance_snapshot congelado. |
conversations | arreglo de object | no | Anclas de contacto de Reg F G5. Cada una lleva source: platform (registrada por este sistema) o cms_import (declarada por el cliente al importar). |
gate_blocks | arreglo de object | no | Todo rechazo duradero (reason_detail congelado). |
consents | arreglo de object | no | |
dnc | object | no | |
dnc.tenant_listings | arreglo de object | no | |
dnc.global_listings | arreglo de object | no | |
dnc.reassigned_numbers | arreglo de object | no | |
cease_and_desist | arreglo de object | no | A nivel de consumidor (alcance "") + por deuda. |
promises | arreglo de object | no | |
callbacks | arreglo de object | no | |
suppression_events | arreglo de object | no | Versión 5 (core#2554). El historial de las exclusiones (opt-out) de los números de la deuda y de su consumer_ref, de la más antigua a la más reciente: kind (dnc_added, dnc_removed, consent_granted, consent_revoked, cease_filed), actor (user:<id>, key:<id>, system:sms_inbound, system:ivr…), source, channel, effective_at y occurred_at. El número se publica sólo con sus últimos cuatro dígitos. Cada evento queda sellado en la cadena del tenant (subject_table: suppression_events, en audit_chain.links). Los eventos existen desde el lanzamiento que introdujo el historial: una supresión anterior no tiene ninguno. |
supervision_actions | arreglo de object | no | |
conference_legs | arreglo de object | no | Versión 3 (core#1701, ADR #223). Los TERCEROS que se unieron a una llamada de la deuda como pierna de conferencia: las filas call_attempts propias de cada pierna, originales y correcciones (corrects_id). No llevan deuda, así que call_attempts nunca las lista; compliance_snapshot.conference_of nombra la llamada a la que se unieron y la corrección de cierre indica cuándo y por qué terminó la pierna (conference_call_ended cuando la llamada terminó primero). |
conference_actions | arreglo de object | no | Versión 3. Los comandos conference_add / conference_remove sobre las llamadas de la deuda: quién fue añadido (destination), la pierna (conf=<uuid> en reason), la atestación del consumidor (consumer_agreed=true) y el resultado. |
policy_versions | arreglo de string | no | |
disclosure_events | arreglo de DisclosureEvent | no | Versión 2 (core#1587). TODAS las atestaciones del aviso de cobranza sobre las llamadas de la deuda, también las que una atestación posterior rectificó (supersedes_id). Indican lo que el agente DECLARÓ. Los eslabones de cadena que las sellan viajan en audit_chain.links (subject_table: call_disclosure_events). |
disclosure_events[].id | string (uuid) | no | |
disclosure_events[].tenant_id | string (uuid) | no | |
disclosure_events[].call_attempt_id | string (uuid) | no | |
disclosure_events[].debt_id | string (uuid) | no | |
disclosure_events[].agent_id | string (uuid) | no | |
disclosure_events[].evidence_kind | agent_attestation | no | |
disclosure_events[].outcome | delivered · not_delivered | no | |
disclosure_events[].reason | string | null | no | |
disclosure_events[].note | string | null | no | |
disclosure_events[].script_sha256 | string | null | no | |
disclosure_events[].script_version_id | string (uuid) | no | Presente sólo cuando la atestación está vinculada a una versión de la biblioteca de guiones (core#1690). |
disclosure_events[].script_resolution | exact · state · debt_type · tenant_default | no | Presente sólo con script_version_id; la rama de precedencia a la que pertenece el selector de la versión para esta llamada. |
disclosure_events[].supersedes_id | string (uuid) | null | no | |
disclosure_events[].occurred_at | string (date-time) | no | |
disclosure_events[].created_at | string (date-time) | no | |
disclosure_summary | object | no | Versión 2. Conteos por intento de la deuda: contacted (en alcance o ya atestado) = attested + unattested, donde attested incluye los declared_not_delivered (listados aparte). Las llamadas undecidable aún no tienen fila final y no están en contacted. |
disclosure_summary.contacted | integer | no | |
disclosure_summary.attested | integer | no | |
disclosure_summary.declared_not_delivered | integer | no | |
disclosure_summary.unattested | integer | no | |
disclosure_summary.undecidable | integer | no | |
disclosure_scripts | arreglo de DisclosureScript | no | Versión 4 (core#1691, ADR #219 Decision 7). Toda versión de guion que alguna atestación de la deuda NOMBRA (script_version_id en una fila de disclosure_events), con el text completo, text_sha256, legal_ref, authored_by, status y los eventos de approval / retirement (actor, reviewed_by, occurred_at). Una versión retirada después de la llamada sigue listada; una atestación que no nombra ninguna versión no añade nada. Vacío cuando ninguna atestación nombra una. |
disclosure_scripts[].id | string (uuid) | sí | |
disclosure_scripts[].tenant_id | string (uuid) | sí | |
disclosure_scripts[].kind | collection_notice | sí | |
disclosure_scripts[].jurisdiction | string | sí | |
disclosure_scripts[].debt_type | string | sí | |
disclosure_scripts[].version | integer | sí | Por tenant y selector. |
disclosure_scripts[].text | string | sí | |
disclosure_scripts[].text_sha256 | string | sí | SHA-256 (hex en minúsculas) de text, derivado por core. |
disclosure_scripts[].legal_ref | string | sí | |
disclosure_scripts[].authored_by | string | sí | |
disclosure_scripts[].status | draft · approved · retired | sí | Se lee de los eventos sellados; si no hay evento approved, es draft. |
disclosure_scripts[].approval | DisclosureScriptEvent | null | no | |
disclosure_scripts[].retirement | DisclosureScriptEvent | null | no | |
disclosure_scripts[].created_at | string (date-time) | no | |
claims | object | no | Versión 2, ampliada en la versión 4. Texto fijo, idéntico en todo paquete: lo que el producto afirma sobre el aviso de cobranza (la declaración del agente) y lo que NO afirma (que el aviso se pronunciara, se oyera o se entendiera, ni que su texto satisfaga una norma). El bloque script (versión 4) dice lo mismo de las versiones de guion: fueron aprobadas por el actor registrado, con la referencia y el revisor registrados, y el producto no afirma que ningún texto satisfaga ninguna norma ni jurisdicción. |
integrity | object | no | |
integrity.algorithm | "sha256" | no | |
integrity.digest | string | no | SHA-256 hex sobre el sobre canónico sin integrity. |
integrity.canonicalization | string | no | |
integrity.worm_attestation | object | null | no |
json
{
"packet": "litigation_defense",
"packet_version": 5,
"tenant_id": "0d4f4f9e-1f2a-4b53-9d3c-8a5e2f7b1c10",
"packet_generated_at": "2026-07-05T10:00:00Z",
"debt": {
"id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
"external_ref": "cms-debt-0001"
},
"contacts": [],
"call_attempts": [],
"conversations": [],
"gate_blocks": [],
"consents": [],
"dnc": {
"tenant_listings": [],
"global_listings": [],
"reassigned_numbers": []
},
"cease_and_desist": [],
"suppression_events": [],
"promises": [],
"callbacks": [],
"supervision_actions": [],
"policy_versions": [
"usa-cell.v1"
],
"integrity": {
"algorithm": "sha256",
"digest": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"canonicalization": "sorted-keys utf-8, no insignificant whitespace",
"worm_attestation": null
}
}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"
}
}Listar los paquetes de defensa ante litigios archivados de una deuda
http
GET /v1/debts/{id}/defense-packet/archivesLas atestaciones de esta deuda, de la más reciente a la más antigua. El token RFC 3161 no va incrustado: viaja en el kit de atestación. Mismo permiso que el GET del paquete.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string (uuid) | sí | UUID del recurso. Los ids mal formados o de otro tenant se leen como 404. |
Respuestas
200
Las atestaciones (lista vacía cuando no se archivó nada).
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
archives | arreglo de DefensePacketArchive | sí | |
archives[].id | string (uuid) | sí | |
archives[].debt_id | string (uuid) | sí | |
archives[].artifact_sha256 | string | sí | SHA-256 (hex) de los bytes del ZIP archivado; el digest que la autoridad selló con marca de tiempo. |
archives[].integrity_digest | string | sí | El integrity.digest propio del paquete (hex) en el momento del archivado. |
archives[].object_key | string | sí | |
archives[].version_id | string,null | no | La versión del objeto que escribió el almacén. Nula sólo para bucket_lock. |
archives[].store | string | sí | |
archives[].retention_mode | COMPLIANCE · GOVERNANCE · bucket_lock | sí | |
archives[].retain_until | string,null (date-time) | no | Fin de retención que confirmó el almacén. Nulo sólo para bucket_lock, donde rige la regla del bucket. |
archives[].tsa_url | string (uri) | sí | |
archives[].tsa_gen_time | string (date-time) | sí | La hora que la autoridad puso en el token (genTime). |
archives[].created_by | string | sí | |
archives[].created_at | string (date-time) | sí |
401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Archivar el paquete de defensa ante litigios (almacén WORM + sello de tiempo RFC 3161)
http
POST /v1/debts/{id}/defense-packet/archivesConstruye el paquete exactamente igual que GET /v1/debts/{id}/defense-packet?format=zip, sella con marca de tiempo el SHA-256 del ZIP en la autoridad RFC 3161 configurada, escribe el ZIP en el almacén de archivo con retención bloqueada y lee la retención de vuelta antes de registrar UNA fila de atestación de sólo anexado. La fila contiene lo que el almacén RESPONDIÓ en la lectura de vuelta (retention_mode, retain_until, version_id), nunca lo que se pidió. Mismo permiso que el GET del paquete. El cuerpo es opcional y su única clave es retention_days; cualquier otra clave es un 400.
Retención: el almacén sella el objeto por el valor por defecto de la plataforma (DEFENSE_RETENTION_DAYS, un valor de despliegue que debe estar definido para activar la función). Un tenant sólo puede ALARGARLA: un retention_days inferior al valor por defecto es un 422, y también lo es uno superior a DEFENSE_RETENTION_MAX_DAYS (cuando éste no está definido, el tenant no puede alargar en absoluto). Nadie puede acortar una retención, y cambiar el valor por defecto sólo afecta a los archivos creados después. La ampliación sólo cuenta para un archivo NUEVO: los bytes ya archivados responden 200 con la fila existente y su retain_until original.
Fail-closed: si el almacén, la autoridad o la lectura de vuelta fallan, NO se escribe ninguna fila y la respuesta es un 503 que indica que no se archivó nada (un objeto que el almacén aceptó antes del fallo queda huérfano y lo elimina el barrido). Idempotente por CONTENIDO: archivar bytes ya archivados para esta deuda responde 200 con la fila existente y no vuelve a llamar a la autoridad ni al almacén. Dos llamadas cuyo ZIP difiere (el paquete se genera con resolución de segundos) son dos archivos. Con tope por deuda y día UTC (DEFENSE_ARCHIVE_MAX_PER_DEBT_PER_DAY, por defecto 5): superado el tope, responde 429 con Retry-After hasta la medianoche UTC. Cada emisión escribe una entrada en el registro de auditoría del tenant.
Con DEFENSE_ARCHIVE_ENABLED desactivado, las tres rutas de archivo responden el mismo 404 que una ruta que no existe.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string (uuid) | sí | UUID del recurso. Los ids mal formados o de otro tenant se leen como 404. |
Cuerpo de la petición (JSON)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
retention_days | integer | no | Retención con la que sellar este archivo, en días. Como mínimo el valor por defecto de la plataforma y como máximo el máximo de la plataforma; si se omite, se usa el valor por defecto. |
json
{
"retention_days": 365
}Respuestas
200
Estos bytes exactos ya estaban archivados; se devuelve la atestación existente.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
archive | object (DefensePacketArchive) | sí | La atestación de sólo anexado de UN paquete archivado. retention_mode, retain_until y version_id son lo que respondió el almacén de archivo cuando se leyó el objeto de vuelta tras la escritura. |
archive.id | string (uuid) | sí | |
archive.debt_id | string (uuid) | sí | |
archive.artifact_sha256 | string | sí | SHA-256 (hex) de los bytes del ZIP archivado; el digest que la autoridad selló con marca de tiempo. |
archive.integrity_digest | string | sí | El integrity.digest propio del paquete (hex) en el momento del archivado. |
archive.object_key | string | sí | |
archive.version_id | string,null | no | La versión del objeto que escribió el almacén. Nula sólo para bucket_lock. |
archive.store | string | sí | |
archive.retention_mode | COMPLIANCE · GOVERNANCE · bucket_lock | sí | |
archive.retain_until | string,null (date-time) | no | Fin de retención que confirmó el almacén. Nulo sólo para bucket_lock, donde rige la regla del bucket. |
archive.tsa_url | string (uri) | sí | |
archive.tsa_gen_time | string (date-time) | sí | La hora que la autoridad puso en el token (genTime). |
archive.created_by | string | sí | |
archive.created_at | string (date-time) | sí |
201
Archivado; la nueva atestación.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
archive | object (DefensePacketArchive) | sí | La atestación de sólo anexado de UN paquete archivado. retention_mode, retain_until y version_id son lo que respondió el almacén de archivo cuando se leyó el objeto de vuelta tras la escritura. |
archive.id | string (uuid) | sí | |
archive.debt_id | string (uuid) | sí | |
archive.artifact_sha256 | string | sí | SHA-256 (hex) de los bytes del ZIP archivado; el digest que la autoridad selló con marca de tiempo. |
archive.integrity_digest | string | sí | El integrity.digest propio del paquete (hex) en el momento del archivado. |
archive.object_key | string | sí | |
archive.version_id | string,null | no | La versión del objeto que escribió el almacén. Nula sólo para bucket_lock. |
archive.store | string | sí | |
archive.retention_mode | COMPLIANCE · GOVERNANCE · bucket_lock | sí | |
archive.retain_until | string,null (date-time) | no | Fin de retención que confirmó el almacén. Nulo sólo para bucket_lock, donde rige la regla del bucket. |
archive.tsa_url | string (uri) | sí | |
archive.tsa_gen_time | string (date-time) | sí | La hora que la autoridad puso en el token (genTime). |
archive.created_by | string | sí | |
archive.created_at | string (date-time) | sí |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}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"
}
}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
Se alcanzó el tope diario de archivado por deuda (code archive_daily_cap). Retry-After es el número de segundos hasta la medianoche UTC. Esta operación también puede responder el 429 de la cuota por clave (rate_limited).
json
{
"error": {
"code": "archive_daily_cap",
"message": "the daily archive cap for this debt is reached; retry after the interval in Retry-After"
}
}503
No se archivó nada (code archive_unavailable): el archivado no está configurado (periodo de retención, almacén o autoridad de marca de tiempo sin definir), o la autoridad, el almacén o la lectura de vuelta de la retención fallaron. Se puede reintentar con seguridad.
json
{
"error": {
"code": "archive_unavailable",
"message": "the archive store did not confirm the retention; nothing was archived"
}
}Kit de atestación de un paquete de defensa ante litigios archivado
http
GET /v1/debts/{id}/defense-packet/archives/{aid}/attestationTodo lo que un tercero necesita para comprobar el archivo sin confiar en esta plataforma: la fila de atestación (attestation.json), la marca de tiempo RFC 3161 como TimeStampResp DER en base64 (artifact.tsr una vez decodificado) y una URL prefirmada de corta duración del ZIP archivado (el version_id exacto). Para verificar: el sha256sum del ZIP descargado debe ser igual a attestation.artifact_sha256, y openssl ts -verify -digest <that sha256> -in artifact.tsr debe aceptarlo frente a la cadena de la autoridad. La URL se genera por petición y caduca en artifact.expires_at. Mismo permiso que el GET del paquete.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string (uuid) | sí | UUID del recurso. Los ids mal formados o de otro tenant se leen como 404. |
aid | path | string (uuid) | sí | El id de la atestación (archives[].id). |
Respuestas
200
El kit de atestación.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
attestation | object (DefensePacketArchive) | sí | La atestación de sólo anexado de UN paquete archivado. retention_mode, retain_until y version_id son lo que respondió el almacén de archivo cuando se leyó el objeto de vuelta tras la escritura. |
attestation.id | string (uuid) | sí | |
attestation.debt_id | string (uuid) | sí | |
attestation.artifact_sha256 | string | sí | SHA-256 (hex) de los bytes del ZIP archivado; el digest que la autoridad selló con marca de tiempo. |
attestation.integrity_digest | string | sí | El integrity.digest propio del paquete (hex) en el momento del archivado. |
attestation.object_key | string | sí | |
attestation.version_id | string,null | no | La versión del objeto que escribió el almacén. Nula sólo para bucket_lock. |
attestation.store | string | sí | |
attestation.retention_mode | COMPLIANCE · GOVERNANCE · bucket_lock | sí | |
attestation.retain_until | string,null (date-time) | no | Fin de retención que confirmó el almacén. Nulo sólo para bucket_lock, donde rige la regla del bucket. |
attestation.tsa_url | string (uri) | sí | |
attestation.tsa_gen_time | string (date-time) | sí | La hora que la autoridad puso en el token (genTime). |
attestation.created_by | string | sí | |
attestation.created_at | string (date-time) | sí | |
tsr | string (byte) | sí | La marca de tiempo RFC 3161 como TimeStampResp DER (estado granted), en base64. |
artifact | object | sí | |
artifact.url | string (uri) | sí | |
artifact.expires_at | string (date-time) | sí |
401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}429 — ApiKeyRateLimited
Esta clave de API ha superado su cuota de peticiones, o las claves del tenant en conjunto han superado el techo del tenant (core#2503) (code rate_limited). La petición no se procesó. Espera Retry-After segundos y reintenta; un rechazo no cuenta contra el presupuesto, así que esa espera siempre basta. Acotado por clave de API y, por encima de las claves, por tenant (api_tenant_requests_per_minute): una clave hermana no se ve afectada hasta que las claves del tenant en conjunto alcanzan ese techo. La cuota se aplica a toda operación alcanzable con una clave dd_, así que las operaciones que nombran un 429 más específico (control de llamadas, disposiciones, SMS, …) PUEDEN responder TAMBIÉN este, con sus cabeceras X-RateLimit-*.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}503
No hay almacén de archivo configurado, así que no se puede generar ninguna URL (code archive_unavailable).
json
{
"error": {
"code": "archive_unavailable",
"message": "archiving is not configured: no archive store is set; nothing was archived"
}
}