Recibir webhooks
Suscribe una URL y recibe eventos firmados en tiempo real de tus bots — nuevas conversaciones, mensajes y escalados.
BotDesk puede enviar eventos HTTP firmados a tu propio endpoint a medida que ocurren cosas en una conversación: empieza una conversación nueva, se persiste un mensaje o el bot escala a una persona. Esto convierte la API pública de solo-consulta en una superficie de integración real: en lugar de hacer polling, recibes los eventos en el momento en que suceden.
Los webhooks salientes están disponibles en el plan Premium.
Autenticación
Todos los endpoints de gestión se autentican con tu API key pública en la
cabecera X-API-Key (la misma clave que usa el resto de la API pública). Cada
endpoint está acotado a tu cuenta: solo puedes ver o modificar tus propias
suscripciones.
Gestionar suscripciones
Ruta base: /v1/public/v1/webhooks
| Método | Ruta | Propósito |
|---|---|---|
POST |
/webhooks |
Crear una suscripción. Devuelve el secreto de firma una sola vez. |
GET |
/webhooks |
Listar tus suscripciones. |
PATCH |
/webhooks/{id} |
Actualizar url, event_types o is_active. |
DELETE |
/webhooks/{id} |
Eliminar una suscripción. |
POST |
/webhooks/{id}/rotate-secret |
Generar un secreto de firma nuevo. Lo devuelve una vez. |
GET |
/webhooks/{id}/deliveries |
Inspeccionar el historial de entregas (estado, código de respuesta, intentos). |
Crear una suscripción
POST /v1/public/v1/webhooks
X-API-Key: <tu-api-key>
Content-Type: application/json
{
"url": "https://tu-app.example.com/botdesk/webhooks",
"event_types": ["conversation.created", "message.created", "escalation.requested"]
}
Respuesta (201 Created):
{
"id": "…",
"url": "https://tu-app.example.com/botdesk/webhooks",
"event_types": ["conversation.created", "message.created", "escalation.requested"],
"is_active": true,
"created_at": "…",
"updated_at": "…",
"secret": "whsec_…"
}
- La
urldebe ser HTTPS y resolver a una dirección pública. Las URLs que apuntan a rangos loopback, link-local, privados o reservados se rechazan con422(protección SSRF), tanto al crear como en cualquier cambio posterior de la URL. - Solo se aceptan los tres tipos de evento de abajo; un valor desconocido
devuelve
422. - El
secretse devuelve exactamente una vez, aquí y enrotate-secret. Guárdalo de forma segura: se usa para verificar firmas y no se vuelve a mostrar.
Eventos
Cada entrega es un POST JSON con un envelope versionado:
{
"event": "message.created",
"event_id": "b3f1c2a4-…",
"version": "v1",
"created_at": "2026-07-29T12:34:56.789012+00:00",
"data": { }
}
| Campo | Significado |
|---|---|
event |
El tipo de evento (uno de los tres de abajo). |
event_id |
Id estable de este evento lógico. Úsalo para deduplicar (ver abajo). |
version |
Versión del envelope, actualmente v1. |
created_at |
Timestamp ISO-8601 en UTC de cuándo se generó el evento. |
data |
Payload específico del evento. |
conversation.created
Se dispara cuando se crea una conversación nueva en cualquier canal (widget, WhatsApp, Telegram, Instagram).
{ "conversation_id": "…", "channel": "whatsapp", "bot_id": "…" }
message.created
Se dispara cada vez que se persiste un mensaje en una conversación, tanto en el turno entrante del usuario como en el turno saliente del bot.
{ "conversation_id": "…", "message_id": "…", "role": "user", "content": "…" }
role es user o assistant. Nota: en el canal widget el mensaje del
asistente se persiste primero como un placeholder vacío y su texto se finaliza
un instante después; el evento lleva el cuerpo del placeholder en el momento de
persistir. Reconcilia por message_id si necesitas la respuesta final.
escalation.requested
Se dispara cuando el bot escala una conversación a una persona (trigger
explícito o implícito). Se incluye contact cuando se capturó información de
contacto.
{ "conversation_id": "…", "contact": { "email": "cliente@example.com" } }
Verificar firmas
Cada entrega lleva estas cabeceras:
| Cabecera | Valor |
|---|---|
X-BotDesk-Signature |
sha256=<hex> — HMAC-SHA256 del mensaje firmado. |
X-BotDesk-Timestamp |
Timestamp Unix (segundos) usado en el mensaje firmado. |
X-BotDesk-Event |
El tipo de evento, reflejando event del cuerpo. |
X-BotDesk-Delivery |
Id único de este intento de entrega. |
El mensaje firmado es el timestamp, un . literal y el cuerpo crudo de la
petición:
signed_message = f"{X-BotDesk-Timestamp}." + <bytes crudos del cuerpo>
expected = "sha256=" + HMAC_SHA256(secret, signed_message).hexdigest()
Compara expected con X-BotDesk-Signature usando una comparación de tiempo
constante. Firma contra los bytes crudos que recibiste: no vuelvas a
serializar el JSON primero, o las diferencias de espacios u orden de claves
romperán la verificación.
Rechaza las entregas cuyo X-BotDesk-Timestamp esté fuera de tu tolerancia (por
ejemplo ±5 minutos) para acotar el replay.
Ejemplo en Python:
import hashlib
import hmac
def verify(secret: str, headers: dict, raw_body: bytes) -> bool:
timestamp = headers["X-BotDesk-Timestamp"]
message = f"{timestamp}.".encode() + raw_body
expected = "sha256=" + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, headers["X-BotDesk-Signature"])
Entrega, reintentos y deduplicación
- Éxito es cualquier respuesta
2xx. Responde rápido (dentro de ~10 segundos) y haz el trabajo lento de forma asíncrona: BotDesk usa un timeout corto y no sigue redirects. - Los reintentos usan un schedule de backoff fijo tras el primer intento:
0s, 1m, 5m, 30m, 2h, 6h. Después de 6 intentos fallidos la entrega se marca dead y no se reintenta más. Inspecciona el estado conGET /webhooks/{id}/deliveries. - Deduplica por
event_id. El mismo evento lógico lleva unevent_idestable en cada reintento y en cada suscripción a la que se propaga. Trata la entrega como at-least-once: un evento puede llegar más de una vez, así que haz tu handler idempotente sobreevent_id. - El orden no está garantizado. Usa
created_at/message_idsi necesitas ordenar los eventos de tu lado.
Rotar y desactivar
POST /webhooks/{id}/rotate-secretemite un secreto nuevo e invalida el anterior. Actualiza tu verificador antes de rotar (o inmediatamente después).- Pon
is_active: falseconPATCHpara pausar las entregas sin eliminar la suscripción; vuelve atruepara reanudar.