Appearance
Mensajería SMS: plantillas aprobadas, bandeja conversacional y respuestas automáticas
English version: SMS messaging.
La mensajería SMS de Dialer Digital ofrece una arquitectura de mensajería con el cumplimiento por delante para agencias de cobranza de EE. UU. La plataforma admite envíos con plantilla y de texto libre, una bandeja conversacional multiusuario con asignación de hilos de sólo-anexar y estados de lectura, y respuestas automáticas fuera de horario.
Disponibilidad: entregado en core desde v0.94.0
Las funciones de mensajería SMS documentadas aquí (plantillas con flujo de aprobación, hilos de la bandeja conversacional y respuestas automáticas fuera de horario) están publicadas en core v0.94.0. Los esquemas de petición y respuesta corresponden estrictamente a la especificación OpenAPI (openapi/dialer-v1.yaml). Confirma con el operador de la plataforma que tu entorno ejecuta v0.94.0 o superior.
Pilares de arquitectura y barandas de cumplimiento
Todas las rutas de mensajería SMS operan bajo barandas de cumplimiento estrictas que fallan cerrado:
Ninguna puerta de cumplimiento se salta: Ya sea que el mensaje se envíe directamente con
POST /v1/sms/messages, desde un hilo de la bandeja conversacional o automáticamente como respuesta fuera de horario, todo envío pasa por el motor de cumplimiento de core (Consent,DNC,QuietHours,SmsFrequency,SpendGuard, frenos de tasa y topes de gasto).- Comportamiento fail-closed: si el consumidor no tiene consentimiento, o si el envío viola el horario de silencio o los límites de frecuencia, el envío se rechaza o se bloquea (
422 compliance_blocked). Cuando el envío llevadebt_idycampaign_id, el bloqueo además deja una fila durable engate_blocks; en ambos casos no llega ningún tráfico al proveedor de SMS aguas arriba. - Exención de palabras clave: sólo las respuestas de sistema a palabras clave (
STOP,HELP,START) disparadas directamente por mensajes entrantes están exentas de las comprobaciones previas al envío, según las directrices de CTIA. Las respuestas automáticas y los mensajes de agente desde la bandeja nunca están exentos. - Un solo acuse de STOP por número y ventana: la baja se aplica en cada STOP, pero el mensaje de confirmación se manda una vez por (tenant, número del consumidor) y ventana, para que un tercero que repita STOP no te facture un SMS por mensaje. La ventana es
sms_stop_ack_window_seconds(ajuste del despliegue, variableSMS_STOP_ACK_WINDOW_SECONDS, por defecto86400s); con0se acusa cada STOP. Lo respaldanlib/dialer/sms/inbound.exyconfig/runtime.exsencore.
- Comportamiento fail-closed: si el consumidor no tiene consentimiento, o si el envío viola el horario de silencio o los límites de frecuencia, el envío se rechaza o se bloquea (
Aplicación de la política de plantillas: Los tenants pueden exigir el uso estricto de plantillas con
PUT /v1/sms/template-policy(templates_only: true). Con ella activa, los envíos de texto libre devuelven422 sms_templates_only.Aviso de baja obligatorio y política de contenido: Todos los borradores de plantilla y los cuerpos de respuesta automática fuera de horario deben incluir de forma explícita un aviso de baja (p. ej.
Reply STOP to opt out) y superar las comprobaciones estáticas de contenido. Los avisos ausentes o los términos prohibidos por los operadores se rechazan con422al crear.Estado auditado de sólo-anexar: Las asignaciones de hilo, los acuses de lectura, las políticas de plantillas y las configuraciones de respuesta automática se registran como bitácoras de eventos o historiales de versiones de sólo-anexar. Ningún registro del libro de mensajes ni de eventos de hilo se modifica ni se borra jamás.
1. Plantillas SMS y flujo de aprobación
Las plantillas SMS permiten definir mensajes estructurados con marcadores de variable (como , o ).
Roles y permisos
- Creación y edición de borradores: cualquier llave de API válida o login del tenant (
memberoaccount_admin). - Aprobación y aplicación de la política: sólo
account_admin. Unmembero una llave de máquinadd_que intente aprobar una plantilla o activar la políticatemplates_onlyrecibe403 role_forbidden.
Ciclo de vida del flujo
[ POST /v1/sms-templates ] ──(Creates draft)──> [ Status: draft ]
│
┌─────────────┴─────────────┐
▼ ▼
[ POST /v1/sms-templates/{id}/approve ] (account_admin) [ POST /v1/sms-templates/{id}/retire ]
│ │
▼ ▼
[ Status: approved ] [ Status: retired ]Endpoints
Crear un borrador de plantilla
POST /v1/sms-templates
Crea un borrador nuevo. Si name ya existe, se crea una nueva versión en borrador.
Cuerpo de la petición:
json
{
"name": "payment-reminder",
"body": "Hi {{first_name}}, your balance of {{amount}} is due on {{due_date}}. Reply STOP to opt out.",
"variables": [
{ "name": "first_name", "type": "string" },
{ "name": "amount", "type": "number" },
{ "name": "due_date", "type": "date" }
]
}- Variables: los tipos declarados deben ser
string(1–160 caracteres),numberodate(ISO-8601). Toda variable usada enbodydebe estar declarada envariables, y viceversa. - Validación: el cuerpo debe contener el aviso de baja y ningún término prohibido.
Listar versiones de plantillas
GET /v1/sms-templates
Lista las versiones de plantilla, con la versión más reciente primero por cada name. Admite los filtros de consulta status (draft, approved, retired) y name.
Obtener una versión de plantilla
GET /v1/sms-templates/{id}
Devuelve el detalle de la plantilla para el ID de versión indicado.
Actualizar un borrador
PATCH /v1/sms-templates/{id}
Actualiza body y/o variables de una plantilla en draft. Editar una plantilla approved o retired devuelve 409 conflict (hay que crear en su lugar un borrador de versión nueva).
Aprobar un borrador de plantilla
POST /v1/sms-templates/{id}/approve
Requiere account_admin. Aprueba el borrador y registra approved_by y approved_at. Aprobar algo que no es un borrador devuelve 409 conflict.
Retirar una versión de plantilla
POST /v1/sms-templates/{id}/retire
Retira una versión de plantilla. Una versión retirada ya no puede usarse para envíos. Retirar un borrador lo descarta.
Política de plantillas («sólo plantillas»)
GET /v1/sms/template-policyPUT /v1/sms/template-policy
Exige que todos los envíos SMS del tenant usen plantillas aprobadas.
PUTrequiereaccount_admin.
Cuerpo de la petición (PUT):
json
{
"templates_only": true
}Con templates_only en true, cualquier intento de enviar SMS de texto libre con POST /v1/sms/messages devuelve 422 sms_templates_only.
2. Bandeja conversacional de SMS (hilos)
La bandeja conversacional agrupa en un solo hilo los mensajes SMS entre el número móvil de un consumidor y una línea del tenant.
Ventana por keyset y modelo de lectura derivado
- Los hilos se derivan al leer a partir del libro subyacente, sobre una ventana deslizante de 31 días.
- Los cambios de estado de entrega de un mensaje no alteran la posición de los hilos; sólo los mensajes originales actualizan la actividad del hilo.
- Los mensajes enviados antes de que existieran los hilos no llevan
thread_idy se consultan por el libro plano (GET /v1/sms/messages).
Endpoints
Listar hilos de la bandeja
GET /v1/sms/threads
Devuelve los hilos SMS activos, del más reciente al más antiguo según el último mensaje original de la ventana.
Parámetros de consulta:
| Parámetro | Tipo | Obligatorio | Descripción |
|---|---|---|---|
from | string (ISO-8601) | no | Inicio de la ventana (por defecto: los últimos 31 días). |
to | string (ISO-8601) | no | Fin de la ventana. |
unread | boolean | no | true filtra a los hilos con mensajes entrantes recibidos después del último evento read del equipo. |
limit | integer | no | Tamaño de página (1–200, por defecto 50). |
cursor | string | no | Cursor de paginación por keyset. |
Cada fila de hilo lleva assigned_to (el usuario de su último evento de asignar/desasignar, o null); la lista no tiene filtro por asignado.
Obtener el detalle de un hilo y sus deudas coincidentes
GET /v1/sms/threads/{id}
Devuelve el detalle del hilo e incluye debt_ids: un arreglo de hasta 200 IDs de deuda del tenant que coinciden con el teléfono del consumidor mediante índice ciego. Un arreglo debt_ids vacío indica que no existen deudas coincidentes para ese número.
Asignar un hilo
POST /v1/sms/threads/{id}/assign
Escribe un evento assigned o unassigned de sólo-anexar, firmado por el login del dashboard autenticado.
Cuerpo de la petición:
json
{
"user_id": "0d4f4f9e-1f2a-4b53-9d3c-8a5e2f7b1c10"
}- Pasa
nullcomouser_idpara liberar la asignación. - Reglas por rol:
member: sólo puede asignarse el hilo a sí mismo, o liberar un hilo asignado a sí mismo. Intentar asignarlo a otro usuario devuelve403 role_forbidden.account_admin: puede asignar el hilo a cualquier usuario activo del tenant.- Las llaves de máquina de la API (
dd_) o los tokens de agente devuelven403 role_forbidden(una asignación exige una identidad de login humana). - Asignar a un usuario deshabilitado, inexistente o de otro tenant devuelve
422 invalid_assignee.
Marcar un hilo como leído
POST /v1/sms/threads/{id}/read
Anexa un evento read firmado por el login del dashboard que llama. Borra el indicador unread para todo el equipo del tenant respecto de los mensajes anteriores a la marca de tiempo de lectura. Requiere un login humano (ddu_); las llaves de máquina reciben 403 role_forbidden.
Listar los mensajes de un hilo
GET /v1/sms/messages?thread_id={id}
Lista los mensajes que pertenecen al hilo indicado.
Responder desde un hilo (envío desde el compositor)
POST /v1/sms/messages
Envía un mensaje saliente. Si se omite from, la plataforma resuelve el default_from_e164 del proveedor. Si no hay una línea por defecto configurada, el envío falla con 422 from_required.
El remitente tiene que ser un número tuyo. El que aplique (from, o el default_from_e164 del proveedor) debe ser un número E.164 y un número ACTIVO del tenant: si no, 422 sms_from_invalid (no es E.164) o 422 sms_from_not_owned (es válido, pero no es un número activo de la cuenta; regístralo en /v1/dids). Con did_ownership_proof en enforce, un número activo sin titularidad verificada se rehúsa con 422 did_unverified. No se encola nada y no se contacta al proveedor. La misma regla rige para el default_from_e164 al registrar o cambiar un proveedor (POST/PATCH /v1/sms/providers) y para el remitente de campaña (sección 4). Lo respaldan lib/dialer/api/v1_router.ex (sms_from_error/2) y lib/dialer/sms/ en core.
Un destino sin tarifa se rehúsa. Si ninguna tarifa de venta cubre la región del envío, se rehúsa con 422 no_rate, tanto en prepago como en pospago. No es un rechazo de pago: recargar no cambia nada, y reintentar sin cambios se rehúsa otra vez hasta que la plataforma ponga precio a esa región. Lo respaldan respond_sms_send/4 en lib/dialer/api/v1_router.ex y la entrada del CHANGELOG de core sobre no_rate en pospago.
3. Respuestas automáticas de SMS fuera de horario
Los tenants pueden configurar respuestas automáticas a los SMS entrantes recibidos fuera del horario de atención establecido.
Requisitos operativos
Una respuesta automática se dispara sólo cuando se cumplen las tres condiciones:
- La bandera de operador
sms_auto_reply_enabledestá entrue. - El horario de atención del tenant (
/v1/tenant/sms-business-hours) está configurado. - Hay una plantilla o cuerpo de respuesta automática (
/v1/tenant/sms-auto-reply) guardado.
Si falta o está desactivada cualquiera de las condiciones, no se genera ninguna respuesta automática.
Comportamiento fail-closed
- QuietHours y puertas de cumplimiento: las respuestas automáticas pasan por
Sender.send_sms/2. Si un texto entrante llega fuera de horario pero el consumidor está en una zona horaria bajoQuietHourso carece de consentimiento, la respuesta automática no se envía y queda registrada engate_blocks. Las respuestas automáticas nunca se encolan para entregarse por la mañana. - Frenos de enfriamiento: las respuestas automáticas aplican un enfriamiento configurable por hilo (
cooldown_seconds, por defecto 86 400 s / 24 horas). Un hilo recibe como máximo una respuesta automática por ventana de enfriamiento.
Endpoints
Obtener el horario de atención
GET /v1/tenant/sms-business-hours
Devuelve el horario de atención SMS configurado, o null si no está configurado.
Fijar el horario de atención
PUT /v1/tenant/sms-business-hours
Requiere account_admin o una llave de máquina dd_. Fija el calendario semanal del horario de atención SMS.
Cuerpo de la petición:
json
{
"timezone": "America/Chicago",
"weekly": {
"mon": { "start": "09:00", "end": "17:00" },
"tue": { "start": "09:00", "end": "17:00" },
"wed": { "start": "09:00", "end": "17:00" },
"thu": { "start": "09:00", "end": "17:00" },
"fri": { "start": "09:00", "end": "17:00" }
}
}Borrar el horario de atención
DELETE /v1/tenant/sms-business-hours
Requiere account_admin o una llave de máquina dd_. Elimina el horario de atención (con lo que se desactivan las respuestas automáticas).
Obtener la configuración de la respuesta automática
GET /v1/tenant/sms-auto-reply
Devuelve la versión activa de la respuesta automática, o null si nunca se guardó.
Guardar una versión de la respuesta automática
PUT /v1/tenant/sms-auto-reply
Requiere account_admin o una llave de máquina dd_. Guarda una versión nueva del texto de la respuesta automática y de su enfriamiento.
Cuerpo de la petición:
json
{
"body": "We are currently closed. We will respond during business hours. Reply STOP to opt out.",
"cooldown_seconds": 86400,
"language": "en"
}- Validación: el cuerpo debe contener un aviso de baja explícito y ningún término prohibido (
422). Si la políticatemplates_onlyestá activa, guardar texto libre devuelve422 sms_templates_only.
4. SMS de campaña: por qué una campaña no envía nada
El remitente de una campaña de SMS (sus caller_ids, su caller_id_pool o, si no hay ninguno, el default_from_e164 del proveedor) sigue la misma regla que un envío directo: debe ser un número ACTIVO del tenant. Una campaña sin remitente utilizable no falla: da vueltas y no envía nada. GET /v1/campaigns/{id}/stats (y el frame campaign.stats) lo explica en stats.send_unavailable_reason: por qué la última vuelta no envió nada, o null si no se detuvo ahí. La lista cerrada, leída de openapi/dialer-v1.yaml de core:
| Valor | Significado |
|---|---|
provider_unresolved | Ningún proveedor de SMS resuelve para la cuenta o la sede; configura uno. |
caller_ids_no_active_did | Ninguno de los caller_ids de la campaña es un número ACTIVO de la cuenta. |
caller_id_pool_no_active_did | El caller_id_pool de la campaña no tiene ningún número activo. |
default_from_invalid | La campaña no tiene remitente propio y el default_from_e164 del proveedor no es un número E.164 de la cuenta. |
did_unverified | did_ownership_proof está en enforce y los únicos números activos desde los que enviaría la campaña no tienen titularidad probada. |
outside_calling_window | La vuelta cayó fuera de la ventana horaria de la campaña; no se envía nada hasta que se abra (no se marca con calling_window_destination_local activo, donde la ventana se decide por contacto). |
Lo limpia la primera vuelta posterior que pasa los controles.
Códigos de error HTTP y referencia del contrato
| Código de estado | code del error | Causa |
|---|---|---|
400 | bad_request | Cuerpo JSON mal formado, ventana > 31 días en la consulta de hilos, campo obligatorio ausente (user_id). |
401 | unauthorized | Token Bearer ausente, inválido o vencido. |
402 | wallet_insufficient | El saldo prepagado no alcanza para este envío. Recarga el saldo y reintenta. |
402 | plan_exhausted | Se agotó la franquicia de SMS incluida en el plan. Sube de plan (o espera al siguiente periodo) y reintenta. |
402 | exposure_limit | Se alcanzó el límite de exposición previo a la verificación. Completa la verificación de la cuenta (KYC) y reintenta. |
403 | role_forbidden | Usuario sin rol de administrador que intenta aprobar una plantilla, cambiar la política, cambiar el horario de atención o asignar un hilo a otro usuario; o llave de máquina que intenta assign/read. |
403 | synthetic_tenant | Envío desde un tenant sintético de demostración, que no origina tráfico saliente. Ningún pago lo cambia; usa un tenant real. |
404 | not_found | El recurso no existe o pertenece a otro tenant. |
409 | conflict | Intento de editar o aprobar una versión de plantilla que no es borrador, o de retirar una versión ya retirada. |
422 | sms_templates_only | Envío de texto libre o respuesta automática cuando la política del tenant exige plantillas aprobadas. |
422 | from_required | Envío sin línea from cuando el proveedor no tiene default_from_e164. |
422 | sms_from_invalid | El remitente (from, o el default_from_e164 del proveedor) no es un número E.164. |
422 | sms_from_not_owned | El remitente es E.164 pero no es un número ACTIVO del tenant. |
422 | did_unverified | El remitente es un número activo del tenant, pero did_ownership_proof está en enforce y su titularidad no está verificada. |
422 | no_rate | Ninguna tarifa de venta cubre la región del destino; se rehúsa y recargar no lo cambia. |
422 | invalid_assignee | Asignación de un hilo a un usuario deshabilitado, inexistente o de otro tenant. |
422 | compliance_blocked | Envío bloqueado por el motor de cumplimiento (Consent, DNC, QuietHours, SmsFrequency). |
429 | throttled / sms_spend_cap_exceeded | Se alcanzó el límite de tasa o el tope mensual de gasto SMS del tenant. |