Appearance
Endpoints de webhook (de cara al proveedor)
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.
Estos son los endpoints públicos, autenticados por firma, que la plataforma expone a los proveedores de SMS (acuses de entrega y mensajes originados en el móvil). No llevan clave bearer: cada petición se verifica contra la firma del proveedor antes de hacer cualquier otra cosa. Para el recorrido de la verificación, consulta Webhooks.
Endpoints
Acuse de entrega — POST /webhooks/sms/{provider_id}/dlr
http
POST /webhooks/sms/{provider_id}/dlrSe sirve en POST /webhooks/sms/{provider_id}/dlr, donde {provider_id} es una fila de /v1/sms/providers. SIN autenticación bearer: firma por proveedor sobre el cuerpo SIN PROCESAR de la petición, verificada en tiempo constante ANTES de cualquier decodificación de JSON:
- Contrato interno (adaptadores
http/mock): cabecerax-dd-signature= HMAC-SHA256-hex(webhook_secret, raw_body). - Adaptador
telnyx: Ed25519 sobre"{telnyx-timestamp}|{raw_body}"con la clave pública de la fila, cabecerastelnyx-signature-ed25519+telnyx-timestamp, ventana de repetición de ±5 min; las cargas útiles son el sobre Telnyx v2 normalizado por el adaptador.
Un id de proveedor desconocido/inactivo/que no es uuid → 404 (sin filtrar su existencia); una firma incorrecta o ausente, o sin secreto configurado → 401 con CERO accesos a tablas. Idempotente: las repeticiones de DLR se colapsan (200 {"status": "duplicate"}); los eventos que verifican pero no se pueden atribuir responden 200 {"status": "ignored"}. Un fallo de escritura en el libro mayor responde 500 para que el proveedor reintente. Cuerpos de más de 1 MB → 413.
Autenticación: ninguna — se autentica por la firma de la petición (ver abajo).
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
x-dd-signature | header | string | no | Contrato interno: HMAC-SHA256 en hexadecimal sobre el cuerpo sin procesar. |
telnyx-signature-ed25519 | header | string | no | Telnyx: firma Ed25519 (base64). |
telnyx-timestamp | header | string | no | Telnyx: marca de tiempo unix firmada junto con el cuerpo (ventana de ±5 min). |
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
message_id | string | sí | Id de mensaje del proveedor tras la ACEPTACIÓN del envío. |
status | sent · delivered · undelivered · failed | sí |
json
{
"message_id": "mock-0001",
"status": "delivered"
}Respuestas
200
Registrado como una fila de estado compensatoria (o repetición colapsada / evento inatribuible).
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
status | ok · duplicate · ignored | sí | duplicate = repetición colapsada en los índices únicos del libro mayor; ignored = autenticado pero inatribuible (registrado + contado, no reintentable). |
ok
json
{
"status": "ok"
}duplicate
json
{
"status": "duplicate"
}ignored
json
{
"status": "ignored"
}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"
}
}Mensaje originado en el móvil — POST /webhooks/sms/{provider_id}/inbound
http
POST /webhooks/sms/{provider_id}/inboundSe sirve en POST /webhooks/sms/{provider_id}/inbound. Mismo modelo de firma que el webhook de DLR (véase smsDeliveryReceipt). Tratamiento de palabras clave (normalizadas, EN+ES): STOP/ALTO/UNSUBSCRIBE/CANCEL/QUIT/END → alta en la lista interna de no llamar (el mismo almacén duradero que leen las compuertas: el siguiente envío se bloquea) + revocación del consentimiento + respuesta de confirmación enlatada; HELP/AYUDA → respuesta de ayuda enlatada; START → nuevo consentimiento (consentimiento concedido otra vez para SMS, alta en la lista interna de no llamar eliminada; las listas regulatorias nunca se tocan); cualquier otra cosa → fila del libro mayor + trama enmascarada sms.received del piso en vivo (lleva el thread_id del mensaje, vacío si no formó ninguno, y nunca el cuerpo). Todo idempotente; los eventos inatribuibles responden 200 {"status": "ignored"}.
Autenticación: ninguna — se autentica por la firma de la petición (ver abajo).
Parámetros
| Nombre | En | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
x-dd-signature | header | string | no | Contrato interno: HMAC-SHA256 en hexadecimal sobre el cuerpo sin procesar. |
telnyx-signature-ed25519 | header | string | no | Telnyx: firma Ed25519 (base64). |
telnyx-timestamp | header | string | no | Telnyx: marca de tiempo unix firmada junto con el cuerpo (ventana de ±5 min). |
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
from | string | sí | Número del consumidor, E.164. |
to | string | sí | El DID del tenant que recibió el mensaje. |
body | string | sí | |
message_id | string | sí |
json
{
"from": "+19995550123",
"to": "+13125550199",
"body": "STOP",
"message_id": "mock-mo-0001"
}Respuestas
200
Tratado (acción de palabra clave, fila del libro mayor, repetición colapsada o ignorado).
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
status | ok · duplicate · ignored | sí | duplicate = repetición colapsada en los índices únicos del libro mayor; ignored = autenticado pero inatribuible (registrado + contado, no reintentable). |
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"
}
}Saliente: un evento v1 enviado por POST a un endpoint de webhook registrado (MT-CTI-04)
http
POST (webhook: crmEvent)Aquí la plataforma es quien LLAMA: el cuerpo es lo que recibe un endpoint de /v1/webhooks suscrito al evento. La fase 2 (core#960, ADR #122) define el sobre y registra una fila por (endpoint, evento) en el outbox webhook_deliveries, escrita en la MISMA transacción que el hecho de dominio; todavía NO se envía NADA: el despachador (fase 3, core#961) vacía el outbox, firma el cuerpo con el secreto del endpoint (X-DD-Signature, más X-DD-Signature-Previous durante una ventana de rotación) y reintenta con un arrendamiento (lease).
data es la proyección pública del recurso, byte a byte la forma que responde su GET, serializada en el momento del hecho y nunca reconstruida: call.ended / call.answered → CallAttempt (la fila de corrección de cierre para call.ended; la fila original con answered_at para call.answered), promise.recorded → Promise, sms.received / sms.optout → SmsMessage (proyección de lista, body_preview de 40 caracteres), recording.available → Recording; disposition.event → DispositionEventData (el efecto emit_event de una regla de disposición); carrier.health_changed → TrunkHealthEvent (una transición de estado de una troncal BYOC, nunca la fila confirm); anomaly.detected → AnomalyDetectedData (la evidencia de una anomalía de insights en el momento en que se abre; nunca al resolverse); ai.drift.detected → AiDriftDetectedData (la evidencia de una alerta de deriva de IA de voz en el momento en que se abre; nunca al cerrarse). event_id es la clave de idempotencia del receptor: el mismo hecho nunca se anuncia dos veces al mismo endpoint. Deduplica por el event_id del cuerpo firmado, NO por la cabecera x-dd-delivery-id: esa cabecera lleva el id de la fila de entrega, viaja fuera de la firma (x-dd-signature cubre el cuerpo) y no es una clave de idempotencia.
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
event | call.ended · call.answered · promise.recorded · sms.received · sms.optout · recording.available · disposition.event · carrier.health_changed · anomaly.detected · ai.drift.detected | sí | El catálogo cerrado v1 de eventos suscribibles (ADR #121). |
event_id | string | sí | Clave de idempotencia, única por endpoint: deduplica por ella. El id de la fila que anuncia el evento (call.ended: la corrección de cierre; promise.recorded: la promesa; sms.*: la fila del libro mayor; recording.available: la grabación; carrier.health_changed: el evento de salud de la troncal; anomaly.detected: la fila de anomalía opened; ai.drift.detected: la fila de deriva opened); call.answered no tiene fila propia y usa <original attempt id>:answered. |
occurred_at | string (date-time) | sí | Cuándo ocurrió el hecho (el ended_at/answered_at de la llamada, el occurred_at del mensaje, ...), no cuándo se envió. |
tenant_id | string (uuid) | sí | |
data | CallAttempt | Promise | SmsMessage | Recording | DispositionEventData | TrunkHealthEvent | AnomalyDetectedData | AiDriftDetectedData | sí | La proyección pública del recurso: la misma forma que responde su GET. |
json
{
"event": "promise.recorded",
"event_id": "019968d2-6b1e-7c4a-9b0e-3f2a1c5d7e90",
"occurred_at": "2026-09-17T20:15:30.123456Z",
"tenant_id": "5a1c3e7d-2f4b-4c8e-9d1a-0b2c3d4e5f60",
"data": {
"id": "019968d2-6b1e-7c4a-9b0e-3f2a1c5d7e90",
"tenant_id": "5a1c3e7d-2f4b-4c8e-9d1a-0b2c3d4e5f60",
"debt_id": "019968d1-0000-7000-8000-000000000001",
"call_attempt_id": null,
"agent_id": null,
"amount_cents": 12500,
"currency": "USD",
"promised_date": "2026-09-27",
"status": "pending",
"note": null,
"created_at": "2026-09-17T20:15:30.123456Z",
"updated_at": "2026-09-17T20:15:30.123456Z"
}
}Respuestas
2XX
Acusado. Cualquier otro estado (o un tiempo de espera agotado) lo reintenta el despachador (fase 3).