Apariencia
Webhooks
Generado desde
DialerDigital/corecommitbbb8e373d900— especificaciónopenapi/dialer-v1.yamlv0.151.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.
Webhooks de salida para CRM: el registro de endpoints del tenant, sus eventos suscritos y el secreto de firma (se muestra una vez; rotación con ventana).
Endpoints
| Método | Ruta | Resumen |
|---|---|---|
GET | /v1/webhooks | Endpoints de webhook de salida |
POST | /v1/webhooks | Registrar un endpoint de webhook de salida |
GET | /v1/webhooks/{id} | Obtener un endpoint |
PATCH | /v1/webhooks/{id} | Actualizar url / events / enabled |
DELETE | /v1/webhooks/{id} | Deshabilitar un endpoint (las filas nunca se borran) |
POST | /v1/webhooks/{id}/rotate-secret | Rotar el secreto de firma (ventana de solapamiento de 24 h) |
GET | /v1/webhooks/{id}/deliveries | Entregas de un endpoint, con cada intento |
POST | /v1/webhooks/{id}/test | Enviar un evento de prueba al endpoint |
POST | /v1/webhooks/{id}/deliveries/{delivery_id}/redeliver | Encolar un intento más para una entrega |
Endpoints de webhook de salida
http
GET /v1/webhooksTodos los endpoints del tenant, habilitados o no, del más antiguo al más reciente. Las respuestas llevan secret_hint (los últimos 4 caracteres del secreto de firma VIGENTE) y NUNCA el secreto. Alcance de tenant (clave dd_ o un login de dashboard); un token de widget ddw_ da 403.
Respuestas
200
Los endpoints del tenant.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
webhooks | arreglo de WebhookEndpoint | sí | |
webhooks[].id | string (uuid) | no | |
webhooks[].url | string | no | |
webhooks[].events | arreglo de WebhookEventName | no | |
webhooks[].enabled | boolean | no | |
webhooks[].disabled_reason | string | null | no | disabled_by_api tras un DELETE; null mientras está habilitado o tras PATCH {"enabled": false} (esa vía no registra motivo). |
webhooks[].secret_hint | string | no | Últimos 4 caracteres del secreto de firma vigente. |
webhooks[].previous_secret_expires_at | string (date-time) | null | no | Fin de la ventana de rotación en la que el secreto anterior aún verifica; null = ningún secreto anterior verifica. |
webhooks[].created_at | string (date-time) | no | |
webhooks[].updated_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"
}
}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 (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. 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 (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"
}
}Registrar un endpoint de webhook de salida
http
POST /v1/webhooksEmite el secreto de firma HMAC del endpoint y lo devuelve una sola vez, como secret de nivel superior de esta respuesta; no vuelve a ser legible (las lecturas muestran secret_hint; POST /v1/webhooks/{id}/rotate-secret emite uno nuevo). El secreto se almacena cifrado con envelope por tenant.
Límites (422 unprocessable). Un tenant puede tener como máximo 20 endpoints habilitados (cada evento escribe una fila de entrega por endpoint); la clave de catálogo webhook_max_endpoints lo eleva por tenant. Los endpoints deshabilitados no cuentan, y un tenant que ya supera el tope sigue entregando a los que tiene.
url DEBE ser una URL https:// en el puerto 443 cuyo host se resuelva solo a direcciones PÚBLICAS: una IP privada literal, loopback, RFC1918, CGNAT, link-local / metadatos de nube y un nombre que no resuelve se rechazan con 400 que nombra url (protección SSRF, en cerrado). La protección juzga solo las direcciones resueltas: un nombre .internal o de clúster se rechaza por dónde resuelve, nunca por su sufijo. La misma comprobación vuelve a ejecutarse en el momento de la entrega. events es un subconjunto no vacío del catálogo cerrado v1 (call.ended, call.answered, promise.recorded, sms.received, sms.optout, recording.available, disposition.event, carrier.health_changed, anomaly.detected, ai.drift.detected, port_request.ported); un evento desconocido o repetido es 400. recording.available se acepta ya y se emite cuando exista su productor.
Todavía no se entrega nada: el outbox y el dispatcher son las siguientes fases de. tenant_id y secret NO son campos de la petición: un cuerpo que lleve cualquiera de los dos es 400 que lo nombra.
Rol. Escribir en el registro requiere una clave dd_ o un login del dashboard con el rol account_admin. Un login member recibe 403 role_forbidden (antes de validar el cuerpo; no se escribe nada) cuando la clave de catálogo webhook_admin_role_enforced del tenant está activada. Nace desactivada: hasta entonces un member escribe como antes y la plataforma solo lo cuenta (dialer.webhooks.admin_role_would_deny).
Una clave de API test se rechaza con 403 test_environment: esta operación fija el secreto o el destino de los eventos de un endpoint live, y la fila queda intacta.
Cuerpo de la petición (JSON, obligatorio)
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
url | string | sí | https:// en el 443, solo host público (véase arriba). |
events | arreglo de WebhookEventName | sí |
json
{
"url": "https://crm.example.com/hooks/dialerdigital",
"events": [
"call.ended",
"promise.recorded"
]
}Respuestas
201
El endpoint, más el secreto de firma: la PRIMERA de las dos únicas veces que se muestra.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
webhook | object (WebhookEndpoint) | sí | Un endpoint de webhook de salida. NUNCA lleva el secreto de firma: secret_hint son los últimos 4 caracteres del secreto VIGENTE, suficientes para saber cuál tiene el CRM. tenant_id va implícito en el bearer. |
webhook.id | string (uuid) | no | |
webhook.url | string | no | |
webhook.events | arreglo de WebhookEventName | no | |
webhook.enabled | boolean | no | |
webhook.disabled_reason | string | null | no | disabled_by_api tras un DELETE; null mientras está habilitado o tras PATCH {"enabled": false} (esa vía no registra motivo). |
webhook.secret_hint | string | no | Últimos 4 caracteres del secreto de firma vigente. |
webhook.previous_secret_expires_at | string (date-time) | null | no | Fin de la ventana de rotación en la que el secreto anterior aún verifica; null = ningún secreto anterior verifica. |
webhook.created_at | string (date-time) | no | |
webhook.updated_at | string (date-time) | no | |
secret | string | sí | Secreto HMAC con prefijo whsec_. Se muestra UNA vez; guárdalo ahora. |
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 (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. Ningún pago lo cambia.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}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 (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"
}
}Obtener un endpoint
http
GET /v1/webhooks/{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 endpoint (secret_hint, nunca el secreto).
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
webhook | object (WebhookEndpoint) | sí | Un endpoint de webhook de salida. NUNCA lleva el secreto de firma: secret_hint son los últimos 4 caracteres del secreto VIGENTE, suficientes para saber cuál tiene el CRM. tenant_id va implícito en el bearer. |
webhook.id | string (uuid) | no | |
webhook.url | string | no | |
webhook.events | arreglo de WebhookEventName | no | |
webhook.enabled | boolean | no | |
webhook.disabled_reason | string | null | no | disabled_by_api tras un DELETE; null mientras está habilitado o tras PATCH {"enabled": false} (esa vía no registra motivo). |
webhook.secret_hint | string | no | Últimos 4 caracteres del secreto de firma vigente. |
webhook.previous_secret_expires_at | string (date-time) | null | no | Fin de la ventana de rotación en la que el secreto anterior aún verifica; null = ningún secreto anterior verifica. |
webhook.created_at | string (date-time) | no | |
webhook.updated_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"
}
}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 (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. 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 (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"
}
}Actualizar url / events / enabled
http
PATCH /v1/webhooks/{id}url y events repiten la misma validación que la creación. enabled: true vuelve a habilitar un endpoint que DELETE deshabilitó y borra disabled_reason. El secreto NO es un campo aquí: la rotación es su propio verbo, de modo que ninguna escritura puede reemplazar un secreto sin devolver el nuevo. Las claves desconocidas son 400.
enabled: true cuenta como una creación: pasado webhook_max_endpoints endpoints habilitados es 422.
Rol. Escribir en el registro requiere una clave dd_ o un login del dashboard con el rol account_admin. Un login member recibe 403 role_forbidden (antes de validar el cuerpo; no se escribe nada) cuando la clave de catálogo webhook_admin_role_enforced del tenant está activada. Nace desactivada: hasta entonces un member escribe como antes y la plataforma solo lo cuenta (dialer.webhooks.admin_role_would_deny).
Una clave de API test se rechaza con 403 test_environment: esta operación fija el secreto o el destino de los eventos de un endpoint live, y la fila queda intacta.
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 |
|---|---|---|---|
url | string | no | |
events | arreglo de WebhookEventName | no | |
enabled | boolean | no |
json
{
"events": [
"call.ended",
"call.answered",
"sms.received"
]
}Respuestas
200
El endpoint actualizado.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
webhook | object (WebhookEndpoint) | sí | Un endpoint de webhook de salida. NUNCA lleva el secreto de firma: secret_hint son los últimos 4 caracteres del secreto VIGENTE, suficientes para saber cuál tiene el CRM. tenant_id va implícito en el bearer. |
webhook.id | string (uuid) | no | |
webhook.url | string | no | |
webhook.events | arreglo de WebhookEventName | no | |
webhook.enabled | boolean | no | |
webhook.disabled_reason | string | null | no | disabled_by_api tras un DELETE; null mientras está habilitado o tras PATCH {"enabled": false} (esa vía no registra motivo). |
webhook.secret_hint | string | no | Últimos 4 caracteres del secreto de firma vigente. |
webhook.previous_secret_expires_at | string (date-time) | null | no | Fin de la ventana de rotación en la que el secreto anterior aún verifica; null = ningún secreto anterior verifica. |
webhook.created_at | string (date-time) | no | |
webhook.updated_at | string (date-time) | no |
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 (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. 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 (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"
}
}Deshabilitar un endpoint (las filas nunca se borran)
http
DELETE /v1/webhooks/{id}Fija enabled: false con disabled_reason: "disabled_by_api" y conserva la fila: es la evidencia del tenant de lo que se suscribió y cuándo, y el rol de la aplicación no tiene permiso DELETE. Idempotente: un segundo DELETE conserva el motivo original. PATCH {"enabled": true} lo recupera.
Rol. Escribir en el registro necesita una clave dd_ o un login de dashboard con el rol account_admin. Un login member recibe 403 role_forbidden (antes de validar el cuerpo; no se escribe nada) una vez que la llave de catálogo del tenant webhook_admin_role_enforced está activada. Nace apagada: hasta entonces un member escribe como antes y la plataforma solo lo cuenta (dialer.webhooks.admin_role_would_deny).
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 endpoint, ahora deshabilitado.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
webhook | object (WebhookEndpoint) | sí | Un endpoint de webhook de salida. NUNCA lleva el secreto de firma: secret_hint son los últimos 4 caracteres del secreto VIGENTE, suficientes para saber cuál tiene el CRM. tenant_id va implícito en el bearer. |
webhook.id | string (uuid) | no | |
webhook.url | string | no | |
webhook.events | arreglo de WebhookEventName | no | |
webhook.enabled | boolean | no | |
webhook.disabled_reason | string | null | no | disabled_by_api tras un DELETE; null mientras está habilitado o tras PATCH {"enabled": false} (esa vía no registra motivo). |
webhook.secret_hint | string | no | Últimos 4 caracteres del secreto de firma vigente. |
webhook.previous_secret_expires_at | string (date-time) | null | no | Fin de la ventana de rotación en la que el secreto anterior aún verifica; null = ningún secreto anterior verifica. |
webhook.created_at | string (date-time) | no | |
webhook.updated_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"
}
}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 (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. 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 (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"
}
}Rotar el secreto de firma (ventana de solapamiento de 24 h)
http
POST /v1/webhooks/{id}/rotate-secretEmite un secreto nuevo y lo devuelve una sola vez (secret de nivel superior). El secreto anterior sigue verificando hasta previous_secret_expires_at (24 h): las entregas dentro de esa ventana se firman con el secreto nuevo y llevan una segunda firma con el anterior, de modo que un receptor que aún no ha cambiado sigue verificando. Solo los dos últimos secretos verifican alguna vez: una rotación dentro de una ventana abierta reemplaza al anterior. Sin cuerpo de petición.
Rol. Escribir en el registro requiere una clave dd_ o un login del dashboard con el rol account_admin. Un login member recibe 403 role_forbidden (antes de validar el cuerpo; no se escribe nada) cuando la clave de catálogo webhook_admin_role_enforced del tenant está activada. Nace desactivada: hasta entonces un member escribe como antes y la plataforma solo lo cuenta (dialer.webhooks.admin_role_would_deny).
Una clave de API test se rechaza con 403 test_environment: esta operación fija el secreto o el destino de los eventos de un endpoint live, y la fila queda intacta.
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 endpoint (nuevo secret_hint, ventana abierta) más el secreto NUEVO: la segunda y última vez que se muestra un secreto.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
webhook | object (WebhookEndpoint) | sí | Un endpoint de webhook de salida. NUNCA lleva el secreto de firma: secret_hint son los últimos 4 caracteres del secreto VIGENTE, suficientes para saber cuál tiene el CRM. tenant_id va implícito en el bearer. |
webhook.id | string (uuid) | no | |
webhook.url | string | no | |
webhook.events | arreglo de WebhookEventName | no | |
webhook.enabled | boolean | no | |
webhook.disabled_reason | string | null | no | disabled_by_api tras un DELETE; null mientras está habilitado o tras PATCH {"enabled": false} (esa vía no registra motivo). |
webhook.secret_hint | string | no | Últimos 4 caracteres del secreto de firma vigente. |
webhook.previous_secret_expires_at | string (date-time) | null | no | Fin de la ventana de rotación en la que el secreto anterior aún verifica; null = ningún secreto anterior verifica. |
webhook.created_at | string (date-time) | no | |
webhook.updated_at | string (date-time) | no | |
secret | string | sí | El secreto whsec_ NUEVO. Se muestra UNA vez. |
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 (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. 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 (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"
}
}Entregas de un endpoint, con cada intento
http
GET /v1/webhooks/{id}/deliveriesLa evidencia de solo anexar de lo que se envió a este endpoint y lo que respondió, las más recientes primero, paginada por keyset. Un tenant que discute con un CRM («nunca lo recibimos») responde con esto: cada entrega lleva sus attempts en orden, y cada intento el status_code HTTP, los primeros ≤ 1 KiB del CUERPO de la respuesta (response_excerpt, nunca las cabeceras) y los instantes en que la petición empezó y terminó.
status se DERIVA de los intentos, no se almacena: pending cuando aún no se hizo ningún intento, y si no el outcome del ÚLTIMO (retry, delivered, deadletter, endpoint_disabled). El sobre del evento NO forma parte de una fila: data es la proyección pública /v1 del recurso y se puede leer con el propio GET del recurso.
Filtros: event (un nombre del catálogo v1), status (uno de los cinco de arriba), since (RFC-3339, límite inferior inclusivo sobre el created_at de la entrega). Un valor desconocido de event o status da 400 que nombra el conjunto aceptado, nunca una página vacía en silencio. Alcance de tenant (clave dd_ o un login de dashboard); un token de widget ddw_ da 403, y un endpoint de otro tenant da 404.
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. |
event | query | call.ended · call.answered · promise.recorded · sms.received · sms.optout · recording.available · disposition.event · carrier.health_changed · anomaly.detected · ai.drift.detected · port_request.ported | no | |
status | query | pending · retry · delivered · deadletter · endpoint_disabled | no | |
since | query | string (date-time) | no | Solo las entregas creadas en este instante o después. |
limit | query | integer | no | |
cursor | query | string | no | Cursor keyset opaco: el next_cursor de la página anterior. |
Respuestas
200
Una página de entregas, las más recientes primero.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
deliveries | arreglo de WebhookDelivery | sí | |
deliveries[].id | string (uuid) | no | |
deliveries[].endpoint_id | string (uuid) | no | |
deliveries[].event | call.ended · call.answered · promise.recorded · sms.received · sms.optout · recording.available · disposition.event · carrier.health_changed · anomaly.detected · ai.drift.detected · port_request.ported · webhook.test | no | Un evento que una entrega puede llevar: el catálogo cerrado (WebhookEventName) más el evento de sistema webhook.test, que solo añade POST /v1/webhooks/{id}/test (nadie se suscribe a él). |
deliveries[].event_id | string | no | Clave de idempotencia con la que el receptor deduplica; única por endpoint. |
deliveries[].occurred_at | string (date-time) | no | Cuándo ocurrió el HECHO (no cuándo se envió). |
deliveries[].created_at | string (date-time) | no | Cuándo se escribió la entrega, en la propia transacción del hecho. |
deliveries[].status | pending · retry · delivered · deadletter · endpoint_disabled | no | DERIVADO, nunca almacenado: pending cuando la entrega aún no tiene intentos, y si no el outcome de su ÚLTIMO intento. |
deliveries[].attempts | arreglo de WebhookDeliveryAttempt | no | Cada intento, del más antiguo al más reciente. Vacío mientras la entrega está pending. |
deliveries[].attempts[].attempt | integer | no | |
deliveries[].attempts[].outcome | retry · delivered · deadletter · endpoint_disabled | no | |
deliveries[].attempts[].reason | string | no | Vocabulario cerrado: ok, egress_refused, endpoint_gone, endpoint_not_enabled, payload_too_large, unexpected_status, timeout, transport, protocol, body_too_large, exception, y dos que no producen envío: manual_redelivery (el operador pidió uno mediante el endpoint de reentrega) y endpoint_circuit_open (el cortacircuitos del endpoint estaba abierto, así que la entrega se devolvió a la cola para cuando termine; no cuenta como intento). |
deliveries[].attempts[].status_code | integer | null | no | El estado HTTP del receptor; null cuando no llegó respuesta. |
deliveries[].attempts[].started_at | string (date-time) | no | |
deliveries[].attempts[].finished_at | string (date-time) | no | |
deliveries[].attempts[].next_attempt_at | string (date-time) | null | no | Cuándo toca el siguiente intento; no nulo solo en retry. |
deliveries[].attempts[].response_excerpt | string | null | no | Primeros bytes del CUERPO de la respuesta (≤ 1 KiB). Nunca las cabeceras. |
next_cursor | string | null | sí | Cursor de la página siguiente; null en la última. |
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 (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. 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 (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"
}
}Enviar un evento de prueba al endpoint
http
POST /v1/webhooks/{id}/testEnvía UN webhook.test al endpoint ahora y responde con lo que este contestó. La petición es la misma que envía producción — misma firma (x-dd-signature, más x-dd-signature-previous dentro de una ventana de rotación), mismas cabeceras, misma guarda de salida — así que un receptor que verifica esto verifica los eventos reales. El cuerpo es el sobre v1 con event: "webhook.test" y un data sintético mínimo: {"test": true, "webhook_id": …, "sent_at": …}. webhook.test es un evento de sistema fuera del catálogo suscribible: nunca es un evento real con datos inventados, así que un consumidor no puede actuar sobre él por error.
La prueba es evidencia: la entrega y su único intento se añaden a GET /v1/webhooks/{id}/deliveries (event: "webhook.test"). Se juzga una sola vez — sin reintentos, sin cortacircuitos — nunca cuenta en los contadores de intentos y no se puede reenviar (404 en …/redeliver). Una respuesta no 2xx o un fallo de transporte sigue siendo 200 aquí: el resultado está en status (delivered | deadletter) y en attempt.reason.
Límite. Como máximo 5 pruebas por webhook y minuto; la sexta da 429 rate_limited con Retry-After. Un endpoint deshabilitado da 409.
Rol. Igual que el resto del registro: una clave dd_ o un login del dashboard con el rol account_admin; un member recibe 403 role_forbidden una vez activa la clave de catálogo webhook_admin_role_enforced del tenant. Sin cuerpo de petición.
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 intento que se hizo.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
delivery_id | string (uuid) | sí | |
event | webhook.test | sí | |
status | delivered · deadletter | sí | |
attempt | object (WebhookDeliveryAttempt) | sí | Un intento de enviar una entrega. De solo anexar: nunca se edita ni se borra. |
attempt.attempt | integer | no | |
attempt.outcome | retry · delivered · deadletter · endpoint_disabled | no | |
attempt.reason | string | no | Vocabulario cerrado: ok, egress_refused, endpoint_gone, endpoint_not_enabled, payload_too_large, unexpected_status, timeout, transport, protocol, body_too_large, exception, y dos que no producen envío: manual_redelivery (el operador pidió uno mediante el endpoint de reentrega) y endpoint_circuit_open (el cortacircuitos del endpoint estaba abierto, así que la entrega se devolvió a la cola para cuando termine; no cuenta como intento). |
attempt.status_code | integer | null | no | El estado HTTP del receptor; null cuando no llegó respuesta. |
attempt.started_at | string (date-time) | no | |
attempt.finished_at | string (date-time) | no | |
attempt.next_attempt_at | string (date-time) | null | no | Cuándo toca el siguiente intento; no nulo solo en retry. |
attempt.response_excerpt | string | null | no | Primeros bytes del CUERPO de la respuesta (≤ 1 KiB). Nunca las cabeceras. |
latency_ms | integer | 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 (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. 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"
}
}409 — Conflict
Transición de estado no válida, conflicto de unicidad, o un asiento que está ocupado/reservado.
json
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}429
rate_limited: más de 5 pruebas de este webhook en el último minuto. Espera Retry-After segundos. La cuota propia de la clave (ApiKeyRateLimited) también puede responder 429 aquí.
Encolar un intento más para una entrega
http
POST /v1/webhooks/{id}/deliveries/{delivery_id}/redeliverAÑADE un intento retry debido ahora (reason: "manual_redelivery") al flujo de la entrega; el siguiente tick del despachador lo envía. Nada de lo ya registrado se edita: los intentos son de solo anexar en la propia base de datos, de modo que la evidencia en la que se apoya una disputa no se puede reescribir, ni siquiera la plataforma.
202, no 200: el envío ocurre en el siguiente tick del despachador, y su resultado aparece como el SIGUIENTE intento en GET /v1/webhooks/{id}/deliveries. La respuesta lleva el intento que se encoló.
La numeración continúa, así que una entrega que ya agotó la escalera de reintentos recibe exactamente UN envío más: un fallo después de eso se registra como deadletter, no como otro retry.
Un endpoint deshabilitado da 409 que nombra por qué se deshabilitó: el 410 Gone que respondió su receptor (endpoint_gone_410) o la llamada a la API (disabled_by_api); vuelve a habilitarlo antes con PATCH {"enabled": true}. También 409 si se añadió otro intento al mismo tiempo (dos reentregas en carrera, o una en carrera con el despachador): vuelve a leer la entrega antes de pedir otra. Sin cuerpo de petición.
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. |
delivery_id | path | string (uuid) | sí |
Respuestas
202
El intento encolado para el siguiente tick del despachador.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
delivery_id | string (uuid) | sí | |
status | retry | sí | El estado derivado de la entrega tras la anexión. |
attempt | object (WebhookDeliveryAttempt) | sí | Un intento de enviar una entrega. De solo anexar: nunca se edita ni se borra. |
attempt.attempt | integer | no | |
attempt.outcome | retry · delivered · deadletter · endpoint_disabled | no | |
attempt.reason | string | no | Vocabulario cerrado: ok, egress_refused, endpoint_gone, endpoint_not_enabled, payload_too_large, unexpected_status, timeout, transport, protocol, body_too_large, exception, y dos que no producen envío: manual_redelivery (el operador pidió uno mediante el endpoint de reentrega) y endpoint_circuit_open (el cortacircuitos del endpoint estaba abierto, así que la entrega se devolvió a la cola para cuando termine; no cuenta como intento). |
attempt.status_code | integer | null | no | El estado HTTP del receptor; null cuando no llegó respuesta. |
attempt.started_at | string (date-time) | no | |
attempt.finished_at | string (date-time) | no | |
attempt.next_attempt_at | string (date-time) | null | no | Cuándo toca el siguiente intento; no nulo solo en retry. |
attempt.response_excerpt | string | null | no | Primeros bytes del CUERPO de la respuesta (≤ 1 KiB). Nunca las cabeceras. |
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 (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. 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"
}
}409 — Conflict
Transición de estado no válida, conflicto de unicidad, o un asiento que está ocupado/reservado.
json
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}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 (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"
}
}