Appearance
Webhooks
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.
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}/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 (core#2406, 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 (core#2251, adenda del ADR #280). 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 (core#2503) (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 primer nivel de esta respuesta: nunca se puede volver a leer (las lecturas muestran secret_hint; POST /v1/webhooks/{id}/rotate-secret emite uno nuevo). El secreto se almacena cifrado por sobre (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 llave de catálogo webhook_max_endpoints lo sube 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: un literal de IP privada, loopback, RFC1918, CGNAT, enlace local / metadatos de la nube y un nombre que no se resuelve se rechazan con 400 que nombra url (protección SSRF, fail-closed). La protección juzga solo las direcciones resueltas: un nombre .internal o de clúster se rechaza por dónde se resuelve, nunca por su sufijo. La misma comprobación se repite 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); un evento desconocido o repetido da 400. recording.available se acepta ya y se emitirá cuando exista su productor.
Todavía no se entrega nada: el outbox y el despachador son las siguientes fases de MT-CTI-04. tenant_id y secret NO son campos de la petición: un cuerpo que lleve cualquiera de los dos da 400 que lo nombra.
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).
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 (core#2406, 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 (core#2251, adenda del ADR #280). 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 (core#2503) (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 (core#2406, 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 (core#2251, adenda del ADR #280). 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 (core#2503) (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 dan 400.
enabled: true cuenta como una creación: pasados webhook_max_endpoints endpoints habilitados da 422.
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. |
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 (core#2406, 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 (core#2251, adenda del ADR #280). 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 (core#2503) (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 (core#2406, 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 (core#2251, adenda del ADR #280). 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 (core#2503) (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 primer nivel). El secreto anterior sigue verificando hasta previous_secret_expires_at (24 h): las entregas en 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: una rotación dentro de una ventana abierta reemplaza al anterior. Sin cuerpo de petición.
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 (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 (core#2406, 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 (core#2251, adenda del ADR #280). 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 (core#2503) (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 | 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 | no | El catálogo cerrado v1 de eventos suscribibles (ADR #121). |
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 (core#2406, 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 (core#2251, adenda del ADR #280). 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 (core#2503) (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"
}
}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 (core#2406, 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 (core#2251, adenda del ADR #280). 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 (core#2503) (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"
}
}