Skip to content

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:

  1. 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 lleva debt_id y campaign_id, el bloqueo además deja una fila durable en gate_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, variable SMS_STOP_ACK_WINDOW_SECONDS, por defecto 86400 s); con 0 se acusa cada STOP. Lo respaldan lib/dialer/sms/inbound.ex y config/runtime.exs en core.
  2. 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 devuelven 422 sms_templates_only.

  3. 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 con 422 al crear.

  4. 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 (member o account_admin).
  • Aprobación y aplicación de la política: sólo account_admin. Un member o una llave de máquina dd_ que intente aprobar una plantilla o activar la política templates_only recibe 403 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), number o date (ISO-8601). Toda variable usada en body debe estar declarada en variables, 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.

  • PUT requiere account_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_id y 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ámetroTipoObligatorioDescripción
fromstring (ISO-8601)noInicio de la ventana (por defecto: los últimos 31 días).
tostring (ISO-8601)noFin de la ventana.
unreadbooleannotrue filtra a los hilos con mensajes entrantes recibidos después del último evento read del equipo.
limitintegernoTamaño de página (1–200, por defecto 50).
cursorstringnoCursor 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 null como user_id para 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 devuelve 403 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 devuelven 403 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:

  1. La bandera de operador sms_auto_reply_enabled está en true.
  2. El horario de atención del tenant (/v1/tenant/sms-business-hours) está configurado.
  3. 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 bajo QuietHours o carece de consentimiento, la respuesta automática no se envía y queda registrada en gate_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ítica templates_only está activa, guardar texto libre devuelve 422 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:

ValorSignificado
provider_unresolvedNingún proveedor de SMS resuelve para la cuenta o la sede; configura uno.
caller_ids_no_active_didNinguno de los caller_ids de la campaña es un número ACTIVO de la cuenta.
caller_id_pool_no_active_didEl caller_id_pool de la campaña no tiene ningún número activo.
default_from_invalidLa campaña no tiene remitente propio y el default_from_e164 del proveedor no es un número E.164 de la cuenta.
did_unverifieddid_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_windowLa 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 estadocode del errorCausa
400bad_requestCuerpo JSON mal formado, ventana > 31 días en la consulta de hilos, campo obligatorio ausente (user_id).
401unauthorizedToken Bearer ausente, inválido o vencido.
402wallet_insufficientEl saldo prepagado no alcanza para este envío. Recarga el saldo y reintenta.
402plan_exhaustedSe agotó la franquicia de SMS incluida en el plan. Sube de plan (o espera al siguiente periodo) y reintenta.
402exposure_limitSe alcanzó el límite de exposición previo a la verificación. Completa la verificación de la cuenta (KYC) y reintenta.
403role_forbiddenUsuario 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.
403synthetic_tenantEnvío desde un tenant sintético de demostración, que no origina tráfico saliente. Ningún pago lo cambia; usa un tenant real.
404not_foundEl recurso no existe o pertenece a otro tenant.
409conflictIntento de editar o aprobar una versión de plantilla que no es borrador, o de retirar una versión ya retirada.
422sms_templates_onlyEnvío de texto libre o respuesta automática cuando la política del tenant exige plantillas aprobadas.
422from_requiredEnvío sin línea from cuando el proveedor no tiene default_from_e164.
422sms_from_invalidEl remitente (from, o el default_from_e164 del proveedor) no es un número E.164.
422sms_from_not_ownedEl remitente es E.164 pero no es un número ACTIVO del tenant.
422did_unverifiedEl remitente es un número activo del tenant, pero did_ownership_proof está en enforce y su titularidad no está verificada.
422no_rateNinguna tarifa de venta cubre la región del destino; se rehúsa y recargar no lo cambia.
422invalid_assigneeAsignación de un hilo a un usuario deshabilitado, inexistente o de otro tenant.
422compliance_blockedEnvío bloqueado por el motor de cumplimiento (Consent, DNC, QuietHours, SmsFrequency).
429throttled / sms_spend_cap_exceededSe alcanzó el límite de tasa o el tope mensual de gasto SMS del tenant.

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