Appearance
SMS
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.
Qué es hoy el SMS
El envío de SMS a través de tu propio proveedor está disponible hoy a nivel de API; el producto completo de mensajería de cobranza está en la hoja de ruta.
Envío puntual de SMS con compuertas + libro mayor unificado.
Endpoints
| Método | Ruta | Resumen |
|---|---|---|
GET | /v1/tenant/sms-business-hours | El horario de atención del tenant para SMS |
PUT | /v1/tenant/sms-business-hours | Fijar el horario de atención del tenant para SMS |
DELETE | /v1/tenant/sms-business-hours | Borrar el horario de atención del tenant para SMS |
GET | /v1/tenant/sms-auto-reply | La respuesta automática de SMS fuera de horario del tenant |
PUT | /v1/tenant/sms-auto-reply | Guardar una nueva versión de la respuesta automática de SMS |
GET | /v1/sms/messages | Consulta del libro mayor de SMS |
POST | /v1/sms/messages | Envío puntual de SMS con compuertas |
GET | /v1/sms/messages/{id} | Vista colapsada de un solo mensaje |
GET | /v1/sms/threads | Bandeja de SMS: una fila por hilo consumidor/línea |
GET | /v1/sms/threads/{id} | Un hilo de la bandeja de SMS con las deudas del número de su consumidor |
POST | /v1/sms/threads/{id}/assign | Asignar un hilo de la bandeja de SMS a un usuario, o liberarlo |
POST | /v1/sms/threads/{id}/read | Marcar un hilo de la bandeja de SMS como leído |
El horario de atención del tenant para SMS
http
GET /v1/tenant/sms-business-hoursEl horario con el que se lee un texto entrante para decidir si llegó FUERA de él (core#1777, ADR 228), o null cuando el tenant no tiene ninguno. Sin horario, la respuesta automática nunca sale. Lo puede leer una clave dd_ y cualquier login de dashboard.
Respuestas
200
El horario almacenado, o null.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
sms_business_hours | TenantCallingWindow | null | sí |
401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}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 horario almacenado (nunca se responde como «sin horario»).
Fijar el horario de atención del tenant para SMS
http
PUT /v1/tenant/sms-business-hoursReemplaza el horario; el cuerpo tiene la forma de la ventana de llamadas del tenant y lo valida el mismo código. Solo gestión del tenant: una clave dd_ o un login account_admin; un login member recibe 403 role_forbidden. Un cuerpo mal formado da 422 con el motivo y se conserva el horario anterior. La escritura queda auditada. Guardar el horario no activa la respuesta automática.
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
timezone | string | sí | Un nombre de zona IANA. Una zona que no se resuelve es 422. |
weekly | object (CampaignWeeklyWindow) | sí | Ventanas por día de la semana con claves mon…sun; al menos un día. Un día que no es una clave no tiene ventana y está cerrado. |
weekly.mon | object (CampaignDayWindow) | no | La ventana de un día de la semana, dentro del día: start estrictamente antes de end. Se acepta HH:MM y se almacena como HH:MM:SS. Sin desfase: la ventana se lee en la zona en la que la campaña lee la hora. |
weekly.mon.start | string | sí | |
weekly.mon.end | string | sí | |
weekly.tue | object (CampaignDayWindow) | no | La ventana de un día de la semana, dentro del día: start estrictamente antes de end. Se acepta HH:MM y se almacena como HH:MM:SS. Sin desfase: la ventana se lee en la zona en la que la campaña lee la hora. |
weekly.tue.start | string | sí | |
weekly.tue.end | string | sí | |
weekly.wed | object (CampaignDayWindow) | no | La ventana de un día de la semana, dentro del día: start estrictamente antes de end. Se acepta HH:MM y se almacena como HH:MM:SS. Sin desfase: la ventana se lee en la zona en la que la campaña lee la hora. |
weekly.wed.start | string | sí | |
weekly.wed.end | string | sí | |
weekly.thu | object (CampaignDayWindow) | no | La ventana de un día de la semana, dentro del día: start estrictamente antes de end. Se acepta HH:MM y se almacena como HH:MM:SS. Sin desfase: la ventana se lee en la zona en la que la campaña lee la hora. |
weekly.thu.start | string | sí | |
weekly.thu.end | string | sí | |
weekly.fri | object (CampaignDayWindow) | no | La ventana de un día de la semana, dentro del día: start estrictamente antes de end. Se acepta HH:MM y se almacena como HH:MM:SS. Sin desfase: la ventana se lee en la zona en la que la campaña lee la hora. |
weekly.fri.start | string | sí | |
weekly.fri.end | string | sí | |
weekly.sat | object (CampaignDayWindow) | no | La ventana de un día de la semana, dentro del día: start estrictamente antes de end. Se acepta HH:MM y se almacena como HH:MM:SS. Sin desfase: la ventana se lee en la zona en la que la campaña lee la hora. |
weekly.sat.start | string | sí | |
weekly.sat.end | string | sí | |
weekly.sun | object (CampaignDayWindow) | no | La ventana de un día de la semana, dentro del día: start estrictamente antes de end. Se acepta HH:MM y se almacena como HH:MM:SS. Sin desfase: la ventana se lee en la zona en la que la campaña lee la hora. |
weekly.sun.start | string | sí | |
weekly.sun.end | string | sí |
json
{
"timezone": "America/Chicago",
"weekly": {
"mon": {
"start": "09:00",
"end": "17:00"
},
"tue": {
"start": "09:00",
"end": "17:00"
}
}
}Respuestas
200
Almacenada (normalizada).
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
sms_business_hours | object (TenantCallingWindow) | sí | El horario de llamadas propio del tenant (core#1527, clave R19 tenant_calling_window). Solo RESTRINGE: R18 (por destino) y la ventana de la campaña siguen aplicándose. Las horas se leen en timezone (la del tenant, NO la del destino). |
sms_business_hours.timezone | string | sí | Un nombre de zona IANA. Una zona que no se resuelve es 422. |
sms_business_hours.weekly | object (CampaignWeeklyWindow) | sí | Ventanas por día de la semana con claves mon…sun; al menos un día. Un día que no es una clave no tiene ventana y está cerrado. |
sms_business_hours.weekly.mon | object (CampaignDayWindow) | no | La ventana de un día de la semana, dentro del día: start estrictamente antes de end. Se acepta HH:MM y se almacena como HH:MM:SS. Sin desfase: la ventana se lee en la zona en la que la campaña lee la hora. |
sms_business_hours.weekly.mon.start | string | sí | |
sms_business_hours.weekly.mon.end | string | sí | |
sms_business_hours.weekly.tue | object (CampaignDayWindow) | no | La ventana de un día de la semana, dentro del día: start estrictamente antes de end. Se acepta HH:MM y se almacena como HH:MM:SS. Sin desfase: la ventana se lee en la zona en la que la campaña lee la hora. |
sms_business_hours.weekly.tue.start | string | sí | |
sms_business_hours.weekly.tue.end | string | sí | |
sms_business_hours.weekly.wed | object (CampaignDayWindow) | no | La ventana de un día de la semana, dentro del día: start estrictamente antes de end. Se acepta HH:MM y se almacena como HH:MM:SS. Sin desfase: la ventana se lee en la zona en la que la campaña lee la hora. |
sms_business_hours.weekly.wed.start | string | sí | |
sms_business_hours.weekly.wed.end | string | sí | |
sms_business_hours.weekly.thu | object (CampaignDayWindow) | no | La ventana de un día de la semana, dentro del día: start estrictamente antes de end. Se acepta HH:MM y se almacena como HH:MM:SS. Sin desfase: la ventana se lee en la zona en la que la campaña lee la hora. |
sms_business_hours.weekly.thu.start | string | sí | |
sms_business_hours.weekly.thu.end | string | sí | |
sms_business_hours.weekly.fri | object (CampaignDayWindow) | no | La ventana de un día de la semana, dentro del día: start estrictamente antes de end. Se acepta HH:MM y se almacena como HH:MM:SS. Sin desfase: la ventana se lee en la zona en la que la campaña lee la hora. |
sms_business_hours.weekly.fri.start | string | sí | |
sms_business_hours.weekly.fri.end | string | sí | |
sms_business_hours.weekly.sat | object (CampaignDayWindow) | no | La ventana de un día de la semana, dentro del día: start estrictamente antes de end. Se acepta HH:MM y se almacena como HH:MM:SS. Sin desfase: la ventana se lee en la zona en la que la campaña lee la hora. |
sms_business_hours.weekly.sat.start | string | sí | |
sms_business_hours.weekly.sat.end | string | sí | |
sms_business_hours.weekly.sun | object (CampaignDayWindow) | no | La ventana de un día de la semana, dentro del día: start estrictamente antes de end. Se acepta HH:MM y se almacena como HH:MM:SS. Sin desfase: la ventana se lee en la zona en la que la campaña lee la hora. |
sms_business_hours.weekly.sun.start | string | sí | |
sms_business_hours.weekly.sun.end | string | sí |
401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403
role_forbidden: un inicio de sesión de dashboard sin el rol account_admin; forbidden: un token de widget o un tenant suspendido.
422
Horario mal formado; el mensaje nombra el motivo. No se almacena nada.
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"
}
}Borrar el horario de atención del tenant para SMS
http
DELETE /v1/tenant/sms-business-hoursVuelve a no tener horario (idempotente), lo que apaga la respuesta automática. Mismos roles que PUT.
Respuestas
200
Borrada.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
sms_business_hours | null | sí |
401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}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 borrar el horario.
La respuesta automática de SMS fuera de horario del tenant
http
GET /v1/tenant/sms-auto-replyLa última versión guardada de la respuesta que se envía a un texto entrante recibido fuera del horario de atención del tenant (core#1777, ADR 228), o null si nunca se guardó ninguna. Leerla no dice nada sobre si está activa: eso requiere el indicador de operador sms_auto_reply_enabled, el horario de atención y todas las compuertas de la ruta de envío de SMS. Lo puede leer una clave dd_ y cualquier login de dashboard.
Respuestas
200
La última versión, o null.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
sms_auto_reply | SmsAutoReply | null | sí |
401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}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 la respuesta almacenada (nunca se responde como «ninguna»).
Guardar una nueva versión de la respuesta automática de SMS
http
PUT /v1/tenant/sms-auto-replyAñade una versión (las anteriores se conservan; la última está en vigor). Solo gestión del tenant: una clave dd_ o un login account_admin; un login member recibe 403 role_forbidden. El cuerpo se valida como lo valida la ruta de envío de SMS: debe llevar un aviso de baja que el tratamiento de palabras clave entrantes respete y no debe contener un término prohibido; un rechazo es 422 que nombra el campo y no se almacena nada. Un tenant que solo usa plantillas no puede almacenar texto libre (422 sms_templates_only).
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
body | string | sí | El texto enviado. Debe nombrar una palabra de baja que el tratamiento de palabras clave entrantes respete y no debe contener un término prohibido. |
language | string | sí | Una etiqueta de idioma, al estilo en o es-MX. |
cooldown_seconds | integer | no | Como máximo una respuesta automática por hilo en estos segundos (de 1 hora a 30 días). |
json
{
"body": "We are closed right now. Reply STOP to opt out.",
"language": "en",
"cooldown_seconds": 86400
}Respuestas
200
La versión recién guardada.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
sms_auto_reply | object (SmsAutoReply) | sí | La versión de la respuesta automática de SMS actualmente en vigor. |
sms_auto_reply.body | string | sí | |
sms_auto_reply.language | string | sí | |
sms_auto_reply.cooldown_seconds | integer | sí | |
sms_auto_reply.set_by | string,null (uuid) | sí | El usuario del dashboard que la guardó; null para una clave de API. |
sms_auto_reply.updated_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"
}
}403
role_forbidden: un inicio de sesión de dashboard sin el rol account_admin; forbidden: un token de widget o un tenant suspendido.
422
unprocessable: el mensaje nombra el campo y el motivo; sms_templates_only: el tenant solo acepta plantillas.
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"
}
}Consulta del libro mayor de SMS
http
GET /v1/sms/messagesFILAS del libro mayor (originales Y transiciones de estado; corrects_id las distingue), las más recientes primero, paginadas por keyset como /v1/call_attempts. En las listas, los cuerpos son vistas previas de 40 caracteres (body_preview).
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
direction | query | outbound · inbound | no | |
status | query | queued · sent · delivered · undelivered · failed · received | no | |
campaign_id | query | string (uuid) | no | |
debt_id | query | string (uuid) | no | |
thread_id | query | string (uuid) | no | Solo las filas de un hilo de la bandeja (thread_id de /v1/sms/threads). Un id mal formado da 400; un id de otro tenant no coincide con nada. |
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 del libro mayor.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
sms_messages | arreglo de SmsMessage | sí | |
sms_messages[].id | string (uuid) | no | |
sms_messages[].direction | outbound · inbound | no | |
sms_messages[].status | queued · sent · delivered · undelivered · failed · received | no | |
sms_messages[].from_e164 | string | null | no | |
sms_messages[].to_e164 | string | no | |
sms_messages[].body_preview | string | null | no | |
sms_messages[].segments | integer | null | no | |
sms_messages[].client_ref | string | null | no | |
sms_messages[].provider_id | string (uuid) | null | no | |
sms_messages[].provider_message_id | string | null | no | |
sms_messages[].campaign_id | string (uuid) | null | no | |
sms_messages[].debt_id | string (uuid) | null | no | |
sms_messages[].thread_id | string (uuid) | null | no | Hilo de la bandeja de la fila; null en filas escritas antes de que existieran los hilos. |
sms_messages[].occurred_at | string (date-time) | no | |
sms_messages[].corrects_id | string (uuid) | null | no | |
sms_messages[].recorded_at | string (date-time) | no | |
next_cursor | string | null | sí |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}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"
}
}Envío puntual de SMS con compuertas
http
POST /v1/sms/messagesEnruta UN mensaje por EL punto de estrangulamiento (Dialer.Sms.Sender, el mismo Originator que las campañas): tope de gasto → motor de cumplimiento (Consent → DNC → QuietHours → SmsFrequency, fail-closed) → fila queued del libro mayor con la evidencia primero → adaptador del proveedor → débito de sms_segment cuando el proveedor ACEPTA. La API no puede saltarse una compuerta más de lo que puede hacerlo una campaña.
client_ref es la clave de idempotencia de quien llama: los reintentos DEBEN reutilizarla; un client_ref repetido responde 202 con duplicate: true y NO se vuelve a contactar al proveedor. Cuando timezone está ausente, las horas de silencio se resuelven por el NPA del destino; los números irresolubles se bloquean en modo fail-closed. La resolución del proveedor recorre sede → toda la empresa → configuración del despliegue; sin proveedor → 422 sms_provider_unconfigured. Una petición sin from cuyo proveedor resuelto no tiene default_from_e164 → 422 from_required: no se encola nada y no se contacta al proveedor (no se puede decidir la línea de la conversación, así que el envío falla en modo cerrado). El remitente (from, o el default_from_e164 del proveedor) debe ser E.164 y un número ACTIVO del tenant: en caso contrario 422 sms_from_invalid / 422 sms_from_not_owned; no se encola nada y no se contacta al proveedor (palanca por tenant sms_from_ownership_enforced, por defecto true; core#2329). Una fila que la resolución selecciona pero cuya base_url ya no responde como un endpoint https en el puerto 443 en una dirección pública → 422 sms_provider_base_url_rejected: la protección SSRF vuelve a resolver el host en el momento del ENVÍO (no solo en el registro), así que un nombre que después apunta a loopback, RFC1918, CGNAT o enlace local se detecta aquí y el envío falla en modo cerrado en lugar de recurrir a otro endpoint. Un tenant prepago cuyo envío no tiene tarifa de venta (no hay tarifa de segmento de SMS para la región) → 422 no_rate: rechazado, pero no es un rechazo de pago: recargar no cambia nada, y reintentar sin cambios volverá a rechazar.
Antes de todo eso (antes de la compuerta del plan, los frenos y el motor) se comprueba el propio cuerpo, con CERO llamadas al proveedor y CERO filas en el libro mayor ante un rechazo. Un cuerpo que no nombra ninguna palabra de baja respetada → 422 sms_optout_notice_missing. Un cuerpo que nombra un término prohibido por los carriers (SHAFT: sexo, odio, alcohol, armas de fuego, tabaco; alivio de deudas, consolidación, condonación, reparación de crédito; palabras completas, sin distinguir mayúsculas) → 422 sms_content_prohibited (ADR #181). Ninguno tiene interruptor por tenant ni por campaña; reformular el cuerpo es la única vía.
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
to | string | sí | Destino, E.164. |
body | string | sí | Texto del mensaje (incluye el aviso de baja STOP; ningún término prohibido por los carriers, véase arriba). |
from | string | no | Remitente E.164 / short code. Opcional: cuando está ausente, core usa el default_from_e164 del proveedor resuelto y envía POR esa línea, de modo que el saliente y la respuesta del consumidor comparten un hilo. Si no hay ninguno → 422 from_required. Sea cual sea el que aplique, debe ser E.164 y un número ACTIVO del tenant (422 sms_from_invalid / 422 sms_from_not_owned). Con la clave del tenant did_ownership_proof en enforce (core#2415), un número customer cuya titularidad nunca se verificó da 422 did_unverified. |
client_ref | string | no | Clave de idempotencia (por defecto, un uuid nuevo). |
debt_id | string (uuid) | no | Atribución (con campaign_id, también deja la fila duradera gate_blocks cuando hay un bloqueo). |
campaign_id | string (uuid) | no | |
consumer_ref | string | no | |
timezone | string | no | Zona horaria IANA para las horas de silencio; si está ausente, se usa el NPA como respaldo. |
json
{
"to": "+19995550123",
"body": "postman probe. Responda STOP para no recibir mas mensajes.",
"client_ref": "postman-1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d"
}Respuestas
202
Aceptado (queued → sent, facturado al ACEPTAR), o client_ref repetido (duplicate: true, NO se vuelve a contactar al proveedor).
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
sms_message | SmsMessageView | null | sí | |
duplicate | boolean | sí |
json
{
"sms_message": {
"id": "0e9f8a7b-6c5d-4e3f-2a1b-0c9d8e7f6a5b",
"direction": "outbound",
"status": "sent",
"from_e164": "+13125550199",
"to_e164": "+19995550123",
"body": "postman probe. Responda STOP para no recibir mas mensajes.",
"segments": 1,
"client_ref": "postman-1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"provider_id": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
"provider_message_id": "mock-0001",
"campaign_id": null,
"debt_id": null,
"compliance_snapshot": {
"decision_id": "2c1d0e9f-8a7b-6c5d-4e3f-2a1b0c9d8e7f"
},
"occurred_at": "2026-07-05T10:00:00Z",
"timestamps": {
"queued": "2026-07-05T10:00:00Z",
"sent": "2026-07-05T10:00:01Z"
},
"transitions": []
},
"duplicate": false
}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"
}
}422 — ComplianceBlocked
El motor de cumplimiento rechazó la operación (fail-closed). gate nombra la compuerta que rechazó; decision_id cita la decisión de la cadena de auditoría: adjúntalo a cualquier disputa. Ocurrieron CERO llamadas al proveedor y CERO filas en el libro mayor. NO reintentes sin cambios: el bloqueo es evidencia, no un error transitorio. (Este 422 es distinto del 422 genérico unprocessable y de sms_provider_unconfigured, que comparten código de estado pero difieren en code.)
json
{
"error": {
"code": "compliance_blocked",
"message": "the compliance engine refused this send (dnc_listed)",
"gate": "dnc_listed",
"decision_id": "2c1d0e9f-8a7b-6c5d-4e3f-2a1b0c9d8e7f"
}
}429 — SmsRateLimited
Frenos de dinero y de tasa, ambos reintentables, pero con semánticas distintas. codesms_spend_cap_exceeded: el gasto de SMS del mes hasta la fecha está en el tope del tenant; reintenta solo tras subir el tope o tras el cambio de mes. code throttled: freno de tasa; reintenta más tarde con espera creciente. No se fija cabecera Retry-After: el límite de envío no sabe cuándo vuelve la capacidad, y el tope de gasto tampoco (una subida del tope o el cambio de mes). Por encima de la cuota por clave, esta operación responde rate_limited como en ApiKeyRateLimited.
spend_cap
json
{
"error": {
"code": "sms_spend_cap_exceeded",
"message": "month-to-date SMS spend is at the tenant cap"
}
}throttled
json
{
"error": {
"code": "throttled",
"message": "SMS send throttled; retry later"
}
}502 — SmsProviderError
El proveedor de SMS rechazó el mensaje: el envío se encoló (evidencia primero) y queda evidenciado como una transición failed; nunca se factura. sms_message_id apunta a la fila del libro mayor.
json
{
"error": {
"code": "provider_error",
"message": "the SMS provider rejected the message (:timeout)",
"sms_message_id": "0e9f8a7b-6c5d-4e3f-2a1b-0c9d8e7f6a5b"
}
}Vista colapsada de un solo mensaje
http
GET /v1/sms/messages/{id}La identidad del original + el estado VIGENTE, los timestamps por estado, el compliance_snapshot congelado, el body completo y las filas de transitions en orden.
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 vista colapsada.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
sms_message | object (SmsMessageView) | sí | La vista COLAPSADA de un solo mensaje: estado vigente, marcas de tiempo por estado, instantánea de cumplimiento congelada, cuerpo COMPLETO, transiciones en orden. |
sms_message.id | string (uuid) | no | |
sms_message.direction | outbound · inbound | no | |
sms_message.status | queued · sent · delivered · undelivered · failed · received | no | |
sms_message.from_e164 | string | null | no | |
sms_message.to_e164 | string | no | |
sms_message.body | string | null | no | |
sms_message.segments | integer | null | no | |
sms_message.client_ref | string | null | no | |
sms_message.provider_id | string (uuid) | null | no | |
sms_message.provider_message_id | string | null | no | |
sms_message.campaign_id | string (uuid) | null | no | |
sms_message.debt_id | string (uuid) | null | no | |
sms_message.thread_id | string (uuid) | null | no | Hilo de la bandeja del mensaje; null en mensajes escritos antes de que existieran los hilos. |
sms_message.compliance_snapshot | object | null | no | |
sms_message.occurred_at | string (date-time) | no | |
sms_message.timestamps | object | no | Marcas de tiempo por estado (p. ej. queued, sent, delivered). |
sms_message.transitions | arreglo de SmsMessage | no | |
sms_message.transitions[].id | string (uuid) | no | |
sms_message.transitions[].direction | outbound · inbound | no | |
sms_message.transitions[].status | queued · sent · delivered · undelivered · failed · received | no | |
sms_message.transitions[].from_e164 | string | null | no | |
sms_message.transitions[].to_e164 | string | no | |
sms_message.transitions[].body_preview | string | null | no | |
sms_message.transitions[].segments | integer | null | no | |
sms_message.transitions[].client_ref | string | null | no | |
sms_message.transitions[].provider_id | string (uuid) | null | no | |
sms_message.transitions[].provider_message_id | string | null | no | |
sms_message.transitions[].campaign_id | string (uuid) | null | no | |
sms_message.transitions[].debt_id | string (uuid) | null | no | |
sms_message.transitions[].thread_id | string (uuid) | null | no | Hilo de la bandeja de la fila; null en filas escritas antes de que existieran los hilos. |
sms_message.transitions[].occurred_at | string (date-time) | no | |
sms_message.transitions[].corrects_id | string (uuid) | null | no | |
sms_message.transitions[].recorded_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"
}
}Bandeja de SMS: una fila por hilo consumidor/línea
http
GET /v1/sms/threadsEl modelo de lectura de la bandeja (core#1775, ADR 228): una fila por hilo (el par ordenado número del consumidor, nuestra línea) con actividad en la ventana, el más reciente primero según el último mensaje original del hilo. DERIVADO en la lectura del libro mayor de solo anexar: sin tabla, contador ni materialización. Una transición de entrega nunca mueve un hilo; solo lo hace un mensaje original.
from/to toman por defecto los últimos 31 días; una ventana de más de 31 días, o con from posterior a to, da 400. El último mensaje mostrado es el último DENTRO de la ventana.
unread=true conserva solo los hilos con un mensaje entrante en la ventana más nuevo que el último evento read del hilo (POST /v1/sms/threads/{id}/read, core#1776); sin evento de lectura, cada entrante cuenta. assigned_to es el usuario al que se asignó el hilo por última vez (POST /v1/sms/threads/{id}/assign), o null.
Los mensajes escritos antes de que existieran los hilos no llevan hilo y nunca aparecen aquí. Una respuesta nunca lleva el resumen de identidad del hilo: thread_id es el único identificador. La lista muestra una vista previa de 40 caracteres del último cuerpo.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
from | query | string (date-time) | no | Límite inferior RFC 3339 (campo específico del endpoint; por defecto los últimos 30 días UTC donde se indica). |
to | query | string (date-time) | no | Límite superior RFC 3339. |
unread | query | boolean | no | true conserva solo los hilos con un mensaje entrante en la ventana. |
limit | query | integer | no | Tamaño de página (por defecto 50, máximo 200). |
cursor | query | string | no | Cursor keyset opaco del next_cursor de la página anterior. |
Respuestas
200
Una página de hilos.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
sms_threads | arreglo de SmsThread | sí | |
sms_threads[].thread_id | string (uuid) | sí | Identidad opaca del hilo; el único identificador (nunca se devuelve el resumen). |
sms_threads[].tenant_id | string (uuid) | sí | |
sms_threads[].consumer_number | string | sí | El número E.164 del consumidor, leído del último mensaje. |
sms_threads[].line_number | string | sí | El número E.164 de nuestra línea. |
sms_threads[].last_message_id | string (uuid) | sí | |
sms_threads[].last_direction | outbound · inbound | sí | |
sms_threads[].last_body_preview | string | null | sí | |
sms_threads[].last_message_at | string (date-time) | sí | |
sms_threads[].unread | boolean | sí | true si la ventana contiene un mensaje entrante del hilo más nuevo que el último evento read del hilo. |
sms_threads[].assigned_to | string (uuid) | null | sí | Usuario al que se asignó el hilo por última vez (el user_id de su último evento de asignación/desasignación); null si nunca se asignó o se liberó. |
next_cursor | string | null | sí |
json
{
"sms_threads": [
{
"thread_id": "5b1c0d2e-3f4a-4b5c-8d6e-7f8091a2b3c4",
"tenant_id": "9c8d7e6f-5a4b-4c3d-2e1f-0a9b8c7d6e5f",
"consumer_number": "+19995550123",
"line_number": "+13125550199",
"last_message_id": "0e9f8a7b-6c5d-4e3f-2a1b-0c9d8e7f6a5b",
"last_direction": "inbound",
"last_body_preview": "Gracias, pago el viernes. Me pueden llamar d…",
"last_message_at": "2026-10-07T10:00:00Z",
"unread": true,
"assigned_to": null
}
],
"next_cursor": null
}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"
}
}Un hilo de la bandeja de SMS con las deudas del número de su consumidor
http
GET /v1/sms/threads/{id}La fila de lista de /v1/sms/threads para un hilo, más debt_ids: las deudas del tenant cuyo contacto lleva el número del consumidor (coincidencia por índice ciego dentro del tenant), 200 como máximo, ordenadas por id. Un debt_ids vacío significa que ninguna deuda tiene ese número; nunca es «sin filtro». A diferencia de la lista, no hay ventana: el último mensaje es el último original del hilo, así que un hilo antiguo también se abre; unread usa los últimos 31 días (tras el último evento read). Los cuerpos completos solo los sirve /v1/sms/messages/{id}; lista las filas del hilo con /v1/sms/messages?thread_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
El hilo.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
sms_thread | any | sí | Un hilo de la bandeja más las deudas que llevan el número del consumidor. |
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"
}
}Asignar un hilo de la bandeja de SMS a un usuario, o liberarlo
http
POST /v1/sms/threads/{id}/assignEscribe UN evento de solo anexar (assigned, o unassigned cuando user_id es null) firmado por el login que llama; el estado nunca se sobrescribe. assigned_to en /v1/sms/threads es el user_id del último evento de asignación/desasignación del hilo. Asignar de nuevo, incluso al mismo usuario, escribe otro evento.
Requiere un login de dashboard (ddu_): una clave dd_ o un token de agente no tiene persona que firme y recibe 403 role_forbidden. Un member puede asignar el hilo solo a sí mismo y liberar solo un hilo que tiene (403 role_forbidden en caso contrario); un account_admin puede asignarlo a cualquier usuario ACTIVO del mismo tenant. Un usuario de otro tenant, un usuario desconocido y un usuario disabled dan todos 422 invalid_assignee. El cuerpo debe llevar user_id (un UUID, o null para liberar); omitirlo da 400.
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string (uuid) | sí | UUID del recurso. Los ids mal formados o de otro tenant se leen como 404. |
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
user_id | string (uuid) | null | sí | Usuario al que asignar el hilo; null lo libera. |
Respuestas
201
El evento registrado.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
sms_thread_event | object (SmsThreadEvent) | sí | Un evento de solo anexar de un hilo de la bandeja, tal como se registró. |
sms_thread_event.thread_id | string (uuid) | sí | |
sms_thread_event.kind | assigned · unassigned · read | sí | |
sms_thread_event.user_id | string (uuid) | null | sí | El asignado de un evento assigned; null para unassigned y read. |
sms_thread_event.actor_user_id | string (uuid) | sí | El login que escribió el evento. |
sms_thread_event.occurred_at | string (date-time) | sí |
400 — BadRequest
Petición mal formada (errores de forma/tipo, filtros no válidos, marcas de tiempo erróneas).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Clave de API ausente, desconocida o revocada (o, en /v1/admin/*, un token de administración incorrecto o sin definir; en webhooks, una firma incorrecta o ausente). Una sesión del dashboard (ddu_) que un inicio de sesión más reciente de la misma persona haya cerrado responde en cambio con el código session_superseded (indicador de tenant single_session_enforced), para que la pantalla pueda explicar por qué se cerró la sesión; un cierre de sesión, un token vencido o uno desconocido siguen siendo unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
El tenant está suspendido. Una sesión del dashboard (ddu_) de un tenant cuya política de inicio de sesión (ADR 227) está en modo enforce responde 403 con el código login_policy_denied ante una solicitud desde una dirección o en una hora que la política no permite — el mismo cuerpo en ambos casos; la sesión no se revoca y una solicitud posterior dentro de la política se procesa. Un token de widget (ddw_) de un puesto queda sujeto a la misma política en cada solicitud (core#2406, indicador widget_login_honors_policy). Un tenant sintético (demo) responde 403 con el código synthetic_tenant en toda ruta que llegaría a un tercero — marcación manual, pierna de transferencia externa o de conferencia, envío de SMS (core#2251, adenda del ADR #280). Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Id desconocido, mal formado (no UUID) O id de otro tenant: RLS devuelve cero filas, así que los tres son indistinguibles por diseño (sin fuga de existencia, nunca un 403 para ids ajenos).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}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"
}
}Marcar un hilo de la bandeja de SMS como leído
http
POST /v1/sms/threads/{id}/readEscribe UN evento read de solo anexar firmado por el login que llama (sin cuerpo). unread en /v1/sms/threads cuenta entonces solo los mensajes entrantes que llegaron después del último evento read del hilo. Lo puede hacer cualquier login de dashboard del tenant; una clave dd_ o un token de agente recibe 403 role_forbidden.
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
201
El evento registrado.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
sms_thread_event | object (SmsThreadEvent) | sí | Un evento de solo anexar de un hilo de la bandeja, tal como se registró. |
sms_thread_event.thread_id | string (uuid) | sí | |
sms_thread_event.kind | assigned · unassigned · read | sí | |
sms_thread_event.user_id | string (uuid) | null | sí | El asignado de un evento assigned; null para unassigned y read. |
sms_thread_event.actor_user_id | string (uuid) | sí | El login que escribió el evento. |
sms_thread_event.occurred_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"
}
}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"
}
}