Skip to content

Endpoints de webhook (de cara al proveedor) ​

Generado desde DialerDigital/core commit 0fc18008b3c8 — especificación openapi/dialer-v1.yaml v0.134.0. No se edita a mano; consulta Cómo se construye esta referencia. Descargar la especificación OpenAPI.

English version

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}/dlr

Se 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): cabecera x-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, cabeceras telnyx-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

NombreEnTipoObligatorioDescripción
x-dd-signatureheaderstringnoContrato interno: HMAC-SHA256 en hexadecimal sobre el cuerpo sin procesar.
telnyx-signature-ed25519headerstringnoTelnyx: firma Ed25519 (base64).
telnyx-timestampheaderstringnoTelnyx: marca de tiempo unix firmada junto con el cuerpo (ventana de ±5 min).

Cuerpo de la petición (JSON, obligatorio)

CampoTipoObligatorioDescripción
message_idstringsíId de mensaje del proveedor tras la ACEPTACIÓN del envío.
statussent · delivered · undelivered · failedsí
json
{
  "message_id": "mock-0001",
  "status": "delivered"
}

Respuestas

200 ​

Registrado como una fila de estado compensatoria (o repetición colapsada / evento inatribuible).

CampoTipoObligatorioDescripción
statusok · duplicate · ignoredsí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}/inbound

Se 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

NombreEnTipoObligatorioDescripción
x-dd-signatureheaderstringnoContrato interno: HMAC-SHA256 en hexadecimal sobre el cuerpo sin procesar.
telnyx-signature-ed25519headerstringnoTelnyx: firma Ed25519 (base64).
telnyx-timestampheaderstringnoTelnyx: marca de tiempo unix firmada junto con el cuerpo (ventana de ±5 min).

Cuerpo de la petición (JSON, obligatorio)

CampoTipoObligatorioDescripción
fromstringsíNúmero del consumidor, E.164.
tostringsíEl DID del tenant que recibió el mensaje.
bodystringsí
message_idstringsí
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).

CampoTipoObligatorioDescripción
statusok · duplicate · ignoredsí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)

CampoTipoObligatorioDescripción
eventcall.ended · call.answered · promise.recorded · sms.received · sms.optout · recording.available · disposition.event · carrier.health_changed · anomaly.detected · ai.drift.detectedsíEl catálogo cerrado v1 de eventos suscribibles (ADR #121).
event_idstringsí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_atstring (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_idstring (uuid)sí
dataCallAttempt | Promise | SmsMessage | Recording | DispositionEventData | TrunkHealthEvent | AnomalyDetectedData | AiDriftDetectedDatasí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).

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