Resumen del módulo de webhooks
Contrato comprobado con el código el 24 de octubre de 2026. Describe funciones implementadas, no planes futuros. Para configurar desde la interfaz, consulta la guía de usuario.
Dos direcciones de las solicitudes
| Dirección | Quién llama | Dirección | Finalidad |
|---|---|---|---|
| API local de gestión | Tu script local → Deskgram | http://127.0.0.1:5000/api/webhooks | Configurar endpoints y consultar entregas |
| Webhook saliente | Deskgram → tu receptor | URL guardada en el endpoint | Enviar eventos mediante POST con JSON |
El host y puerto son un ejemplo del backend local; usa los de tu instalación. No es la API de licencias, el servidor MCP ni los métodos setWebhook o getUpdates de Telegram Bot API.
Estas rutas REST no realizan una comprobación Bearer independiente. Úsalas en un entorno local de confianza; no publiques el puerto del backend para enviar webhooks. La entrega saliente no necesita conexiones entrantes al equipo de Deskgram. Si añadiste una pasarela de autorización, configura sus reglas por separado.
Las respuestas locales son JSON. Al enviar ajustes mediante POST/PUT, utiliza Content-Type: application/json.
Inicio rápido con la API
Los comandos curl usan una shell compatible con comillas simples. En PowerShell utiliza curl.exe y las reglas de tu versión para pasar JSON. Los dominios, ID y secretos son ficticios. El texto ruso de los ejemplos es un valor ilustrativo, no un requisito del contrato.
- Crea un endpoint desactivado para revisar la configuración antes de la entrega automática:
curl -X POST 'http://127.0.0.1:5000/api/webhooks/endpoints' \ -H 'Content-Type: application/json' \ --data '{"name":"CRM: рассылка","url":"https://receiver.example.com/deskgram","secret":"YOUR_SHARED_SECRET","events":["message.sent","message.failed","lead.reply_received"],"module_filter":["spam_pm"],"payload_template":"deskgram","enabled":false}'- Obtén
endpoint.idde la respuesta. Para el ID ficticio12, envía la prueba:
curl -X POST 'http://127.0.0.1:5000/api/webhooks/endpoints/12/test'- Comprueba
success,delivery.responsestatuscode,delivery.response_bodyy la recepción efectiva. La prueba no contiene un lead real y puede no superar la validación comercial del CRM. - Activa el endpoint en la interfaz o envía su configuración completa mediante PUT con
enabled: true. PUT no es una actualización parcial. - Ejecuta una acción real que corresponda al evento elegido y consulta el historial:
curl 'http://127.0.0.1:5000/api/webhooks/deliveries?endpoint_id=12&limit=20'Objeto endpoint
Campos de creación y actualización
POST y PUT usan el mismo modelo.
| Campo | Tipo | Valor predeterminado | Reglas |
|---|---|---|---|
name | string | Obligatorio | 1–120 caracteres antes de recortar espacios; usa un nombre no vacío |
url | string | Obligatorio | 8–2000 caracteres; empieza por http:// o https://; se recomienda HTTPS |
method | string | POST | Solo POST |
secret | string o null | null al crear | Se recortan espacios; en PUT, omitir conserva, null o vacío borra |
headers | object con valores string | {} | Cabeceras adicionales del receptor, no credenciales de Deskgram |
events | array de string | ["task.completed"] | Seis eventos del catálogo o "*"; una lista vacía se sustituye por task.completed |
module_filter | array de string | [] | Vacío significa todos los módulos; ejemplo ["spam_pm"] |
payload_template | string | deskgram | deskgram, shortresult, crmlead o custom |
payload_mapping | object | {} | Configuración de campos descrita abajo |
enabled | boolean | true | Autoriza envíos automáticos de eventos coincidentes |
timeout_seconds | number | 15 | Entre 3 y 60 segundos |
Para un endpoint ordinario, el timeout se aplica a las operaciones de red del cliente HTTP, no al tiempo total estricto de la llamada local. Test y retry esperan la entrega, incluidos intentos automáticos y pausas.
Los nombres de módulos pasan a minúsculas y sustituyen - por _. Se eliminan duplicados de eventos y módulos. "*" abarca los eventos que realmente llegan al mecanismo; no incorpora nuevas integraciones a otros módulos.
Campos de respuesta
Además de los ajustes, se devuelven id integer, hassecret boolean, createdat y updated_at en UTC. secret siempre es null, aunque haya uno guardado.
La API REST local devuelve los valores de headers sin ocultarlos. Pueden contener tokens del receptor: no publiques la respuesta completa en registros o consultas de soporte.
Ejemplo de creación:
{ "success": true, "endpoint": { "id": 12, "name": "CRM: рассылка", "url": "https://receiver.example.com/deskgram", "method": "POST", "secret": null, "has_secret": true, "headers": {}, "events": ["message.sent", "message.failed", "lead.reply_received"], "module_filter": ["spam_pm"], "payload_template": "deskgram", "payload_mapping": {}, "enabled": false, "timeout_seconds": 15.0, "created_at": "2026-10-15T10:00:00Z", "updated_at": "2026-10-15T10:00:00Z" }}Métodos de la API
Las rutas incluyen /api. Una operación completada usa HTTP 200, también al crear o borrar. endpointid y deliveryid son ID enteros de registros.
GET /api/webhooks/endpoints
Lista los endpoints del más reciente al más antiguo por fecha de creación. No hay parámetros de paginación. Respuesta: {"endpoints": [...]} con objetos endpoint; si está vacía, {"endpoints": []}.
POST /api/webhooks/endpoints
Crea un endpoint con el modelo anterior. Respuesta: {"success": true, "endpoint": {...}}.
Si omites enabled, queda activado inmediatamente. Envía false explícitamente durante la configuración inicial.
PUT /api/webhooks/endpoints/{endpoint_id}
Actualiza el endpoint. Requiere name, url y los demás ajustes actuales que quieras conservar. Los campos omitidos vuelven a sus valores predeterminados, excepto secret.
No reenvíes el objeto de respuesta sin procesarlo: su secret: null eliminaría el secreto guardado. Omite completamente esa clave para conservarlo. Null o una cadena vacía lo borran.
Ejemplo de activación conservando el secreto:
{ "name": "CRM: рассылка", "url": "https://receiver.example.com/deskgram", "method": "POST", "headers": {}, "events": ["message.sent", "message.failed", "lead.reply_received"], "module_filter": ["spam_pm"], "payload_template": "deskgram", "payload_mapping": {}, "enabled": true, "timeout_seconds": 15}Respuesta: {"success": true, "endpoint": {...}}. Endpoint inexistente: HTTP 404.
DELETE /api/webhooks/endpoints/{endpoint_id}
Borra el endpoint sin cuerpo de solicitud. Respuesta: {"success": true, "endpoint_id": 12}. Si no existe: HTTP 404.
El historial permanece. Borrar no cancela un envío ya iniciado; no se puede reintentar una entrega después de borrar su endpoint.
POST /api/webhooks/endpoints/{endpoint_id}/test
Envía una prueba únicamente al endpoint indicado. No requiere cuerpo y no permite proporcionar un payload de prueba arbitrario.
Funciona también con endpoints desactivados, independientemente de suscripciones y filtros de módulos. Aplica el mapping actual y responde después del envío y sus reintentos automáticos.
Ejemplo de rechazo HTTP 403 del receptor con formato estándar:
{ "success": false, "delivered": 0, "delivery_id": 45, "status": "error", "delivery": { "id": 45, "endpoint_id": 12, "event_type": "webhook.test", "task_id": null, "status": "error", "attempts": 3, "response_status_code": 403, "response_body": "Forbidden", "error": "HTTP 403", "created_at": "2026-10-15T10:01:00Z", "updated_at": "2026-10-15T10:01:07Z", "delivered_at": null, "request_payload": { "schema_version": "1.0", "event": "webhook.test", "occurred_at": "2026-10-15T10:01:00Z", "source": "deskgram", "test": true, "endpoint": {"id": 12, "name": "CRM: рассылка"} }, "source_payload": { "schema_version": "1.0", "event": "webhook.test", "occurred_at": "2026-10-15T10:01:00Z", "source": "deskgram", "test": true, "endpoint": {"id": 12, "name": "CRM: рассылка"} } }}El estado de la API local sigue siendo HTTP 200. El 403 pertenece al receptor y aparece dentro de delivery. Si funciona, devuelve success: true, delivered: 1 y status: "success".
Si no se creó una entrega, delivery_id y delivery pueden ser null y success false. Un endpoint inexistente produce HTTP 404 local.
GET /api/webhooks/deliveries
Lista entregas de nuevas a antiguas por fecha de creación.
| Parámetro query | Tipo | Predeterminado | Significado |
|---|---|---|---|
endpoint_id | integer | No indicado | ID del endpoint |
event_type | string | No indicado | Nombre exacto, por ejemplo message.failed |
status | string | No indicado | pending, retrying, success o error |
limit | integer | 100 | De 1 a 500 registros |
Respuesta: {"deliveries": [...]}. Cada elemento es una entrega sin requestpayload ni sourcepayload. No proporciona offset, cursor ni cantidad total.
GET /api/webhooks/deliveries/{delivery_id}
Obtiene los detalles, incluidos requestpayload y sourcepayload. Respuesta: {"delivery": {...}}. Entrega inexistente: HTTP 404.
POST /api/webhooks/deliveries/{delivery_id}/retry
Reintenta manualmente una entrega en estado error. No requiere cuerpo. Crea un registro nuevo y espera sus intentos. También se permite con un endpoint desactivado.
La respuesta incluye success boolean, originaldeliveryid integer, status string y delivery completo de la nueva entrega. Su id y X-Deskgram-Delivery-Id cambian; la original permanece. Un fallo del receptor se comunica con HTTP 200 local y success: false.
HTTP 400: la entrega original no está en error o ya se está reintentando manualmente en este proceso. HTTP 404: no existe la entrega o su endpoint.
GET /api/webhooks/events
Catálogo de los seis eventos con sus etiquetas. El ejemplo conserva las etiquetas rusas que devuelve realmente la API; traducir esta documentación no modifica esos valores.
{ "events": [ {"event": "task.completed", "label": "Задача завершена"}, {"event": "task.failed", "label": "Задача завершилась ошибкой"}, {"event": "task.cancelled", "label": "Задача остановлена"}, {"event": "lead.reply_received", "label": "Получен ответ лида"}, {"event": "message.sent", "label": "Сообщение отправлено"}, {"event": "message.failed", "label": "Ошибка отправки сообщения"} ]}webhook.test se envía con el método de prueba y no figura entre las suscripciones. "*" se acepta en ajustes, pero tampoco aparece en el catálogo.
Objeto delivery
| Campo | Tipo | Descripción |
|---|---|---|
id | integer | ID en la base local |
endpoint_id | integer | ID del endpoint, aunque se haya borrado después |
event_type | string | Evento, independiente del mapping saliente |
task_id | string o null | ID de tarea si existe |
status | string | pending, retrying, success o error |
attempts | integer | Intentos automáticos realizados |
responsestatuscode | integer o null | Último estado HTTP; puede ser null por fallo de red |
response_body | string o null | Última respuesta truncada; puede ser null por fallo de red |
error | string o null | Diagnóstico, no un campo de error comercial del CRM |
createdat, updatedat | string | Fechas ISO 8601 UTC terminadas en Z |
delivered_at | string o null | Momento de entrega correcta |
request_payload | object | JSON tras mapping; solo detalles/test/retry |
source_payload | object | Evento original; puede ser {} en registros antiguos; solo detalles/test/retry |
En transporte ordinario se conservan como máximo 2000 caracteres de respuesta; el transporte protegido de MCP también limita los bytes leídos. El historial no es una traza HTTP completa: no guarda las cabeceras enviadas ni todas las respuestas intermedias.
Eventos y JSON saliente
Estos ejemplos corresponden a payload_template: "deskgram". Con mapping cambia la estructura externa; X-Deskgram-Event conserva el nombre del evento.
Eventos finales de tareas
| Evento | Significado |
|---|---|
task.completed | Terminó la ejecución; no garantiza que cada acción fuera correcta |
task.failed | Estado final de error |
task.cancelled | Tarea detenida o cancelada |
{ "schema_version": "1.0", "event": "task.completed", "occurred_at": "2026-10-15T10:05:00Z", "source": "deskgram", "task": { "task_id": "12345", "task_name": "Тестовая рассылка", "module_name": "spam_pm", "status": "completed", "created_at": "2026-10-15T10:04:00Z", "completed_at": "2026-10-15T10:05:00Z", "accounts_count": 1, "params": {}, "statistics": {}, "result": {} }}params, statistics y result dependen del módulo; no existe un esquema universal de contadores. taskname, modulename y created_at pueden ser null. Failed/cancelled mantienen la familia de estructura, con el evento, estado y resultados correspondientes.
Eventos de mensajes en tiempo real
Actualmente message.sent, message.failed y lead.replyreceived están conectados a spampm. Un filtro de otro módulo no crea nuevas fuentes de eventos.
Ejemplo de mensaje enviado:
{ "schema_version": "1.0", "event": "message.sent", "occurred_at": "2026-10-15T10:04:20Z", "source": "deskgram", "module": "spam_pm", "task": { "task_id": "12345", "campaign_id": "spam_pm:12345", "module_name": "spam_pm" }, "account": {"id": "987654321"}, "lead": { "telegram_user_id": "123456789", "username": "example_client", "raw_identifier": "@example_client" }, "message": { "message_id": 1001, "message_text": "Здравствуйте!", "raw_identifier": "@example_client", "text": "Здравствуйте!", "status": "sent" }}lead y message dependen de los datos disponibles. Pueden faltar teléfono, username, nombre, ID o texto. Para mapear texto usa message.text. message_text es adicional de una ruta concreta, no universal.
message.failed incluye el error y estado disponibles en message; puede no existir un ID de mensaje enviado. Omitir destinatarios o fallar al resolverlos no siempre genera el evento.
lead.replyreceived puede incluir messageid, text, media_type, datetime, timestamp y status: "received". Solo existe si un manejador activo recibió realmente la respuesta. El webhook no activa escucha permanente ni garantiza respuestas después de terminar la tarea.
message.sent no confirma lectura. Trata los ID como valores opacos: los de tarea, cuenta y usuario Telegram son strings; un message_id concreto puede ser numérico.
Evento de prueba
Contiene schemaversion, event: "webhook.test", occurredat, source, test: true y endpoint: {id, name}. El método test muestra su estructura completa.
No contiene task, lead, account ni message. Un mapping compuesto solo de fuentes de lead produce {}.
Mapping de campos
El formato deskgram envía el evento original completo e ignora payload_mapping. Selecciona otro formato para limitar campos.
Para shortresult, crmlead y custom se recomienda este mapping explícito:
{ "fields": [ {"target": "event", "source": "event", "enabled": true}, {"target": "contact.telegram_id", "source": "lead.telegram_user_id", "enabled": true}, {"target": "contact.username", "source": "lead.username", "enabled": true}, {"target": "comment", "source": "message.text", "enabled": true}, {"target": "status", "source": ["message.status", "task.status"], "enabled": true} ]}El objeto completo se asigna a payload_mapping. target es una ruta en el receptor; source, una ruta en el evento original. No son constantes ni expresiones ejecutables.
Resultado del ejemplo message.sent:
{ "event": "message.sent", "contact": {"telegram_id": "123456789", "username": "example_client"}, "comment": "Здравствуйте!", "status": "sent"}Reglas:
- Los puntos en target crean objetos anidados. Evita rutas incompatibles como contact y contact.username simultáneamente.
- source lee claves anidadas separadas por puntos. No admite índices de arrays ni plantillas
{{...}}. - Valores ausentes o null se omiten. Un origen único conserva 0, false y cadena vacía con su tipo JSON.
- Una lista toma el primer valor distinto de null, cadena vacía, lista vacía u objeto vacío. También se admite
"message.status|task.status". - enabled: false excluye; omitir enabled significa incluir.
{"fields": []}o todos los campos desactivados producen{}, sin sustituir el payload completo.- Con payloadmapping: {}, shortresult/crm_lead usan campos internos; custom produce {}. fields explícitos fijan el contrato.
shortresult interno contiene los disponibles event, occurredat, taskid, module, status, lead, message, error. crmlead interno contiene event, occurredat, taskid, accountid, telegramuserid, username, phone, firstname, lastname, rawidentifier, messageid, messagetext, messagestatus, messageerror.
La interfaz puede guardar su propia lista editable de fields para el preajuste. Los ajustes guardados del endpoint determinan el contenido real, no solo su nombre.
Cabeceras salientes
| Cabecera | Valor |
|---|---|
Content-Type | application/json |
User-Agent | Deskgram-Webhooks/1.0 |
X-Deskgram-Delivery-Id | ID de entrega como string |
X-Deskgram-Event | Nombre del evento |
x-secret | Secreto compartido, si existe |
X-Deskgram-Webhook-Secret | El mismo secreto |
X-Deskgram-Signature | Firma sha256=, con secreto configurado |
El secreto se utiliza para HMAC y además se envía en dos cabeceras. Usa HTTPS y evita registrarlas públicamente.
headers no puede sustituir Content-Type, User-Agent, Delivery-Id, Event ni, con secreto configurado, las tres cabeceras del secreto, independientemente de mayúsculas. Otras, como Authorization del receptor, se añaden aparte.
Verificación de firma
Algoritmo: HMAC-SHA256(secretutf8, rawrequest_body). Se envía sha256= seguido de digest hexadecimal en minúsculas.
Se firman los bytes exactos enviados. JSON usa UTF-8, sin escape ASCII del texto no ASCII, claves ordenadas y separadores compactos. Usa el cuerpo HTTP original, no JSON analizado y vuelto a serializar.
Ejemplo Python con biblioteca estándar:
import hashlibimport hmacdef verify_deskgram_signature(raw_body: bytes, signature: str, secret: str) -> bool: if not secret or not signature: return False expected = "sha256=" + hmac.new( secret.encode("utf-8"), raw_body, hashlib.sha256 ).hexdigest() try: return hmac.compare_digest(expected, signature) except TypeError: return FalsePasa bytes originales, X-Deskgram-Signature y el secreto guardado en el receptor. Si falla, no ejecutes la acción comercial. Carga el secreto de configuración protegida, no del x-secret entrante.
La firma sola no impide reutilizar un cuerpo firmado. El receptor necesita comprobación de antigüedad y deduplicación. Delivery-Id no pertenece al cuerpo firmado. Se recortan espacios del secreto; usa uno ASCII no vacío, sin espacios exteriores y de hasta 1000 caracteres.
Reintentos y garantías
- Solo HTTP 200–299 es correcto; no se siguen redirecciones. Un 200 con error comercial en JSON sigue siendo éxito de transporte.
- Hasta tres intentos, con pausas aproximadas de 2 y 4 segundos antes del segundo y tercero. Se repiten errores de red y HTTP, incluidos 4xx. Retry-After no se procesa por separado.
- Los intentos automáticos conservan cuerpo y Delivery-Id. El receptor debe evitar repetir una acción ya completada.
- Retry manual utiliza URL, secreto y headers actuales. Si existe sourcepayload, aplica mapping actual al evento original; registros antiguos sin él reutilizan requestpayload sin volver a mapear.
- El retry manual crea otro Delivery-Id. Añade deduplicación comercial mediante identificadores de tarea/cuenta/mensaje y tipo de evento, según el caso.
- Delivery-Id es único en una base local, no en todas las instalaciones. Distingue los remitentes si recibes de varias.
- No se garantiza exactly-once, orden entre eventos ni recuperación automática tras reiniciar. Un registro pending/retrying interrumpido no implica que se reanude.
- Desactivar evita nuevos eventos automáticos, no cancela envíos iniciados; test/retry explícitos pueden enviar aun desactivado.
Flujo recomendado: validar firma, almacenar de forma fiable o encolar, responder 2xx y realizar procesamiento largo aparte.
Errores de la API
| HTTP local | Situación |
|---|---|
| 200 | Operación completada; en test/retry revisa también success y delivery |
| 400 | Método/formato/evento no admitido, URL sin http(s), retry inválido |
| 404 | Endpoint/entrega inexistentes o endpoint original borrado |
| 422 | Error de validación del cuerpo, tipos o límites de limit/timeout |
Ejemplo de error; el servidor puede conservar texto ruso:
{"detail": "Webhook endpoint не найден"}En validación, detail puede ser un array de diagnósticos FastAPI en vez de string. Un fallo de base de datos o backend puede producir error del servidor, distinto de la respuesta del CRM.
Diferencias con MCP
MCP es otra interfaz de gestión para un agente IA, no un sinónimo de REST. Consulta la conexión MCP.
| Propiedad | REST local | Herramientas MCP |
|---|---|---|
| Nuevo endpoint | enabled=true predeterminado | Desactivado |
| Vista previa | Ejecuta la solicitud | Crear/cambiar suele usar dry-run según ajustes MCP |
| Actualización | PUT completo, excepción secret | Parcial |
| URL | http(s); no publiques la API local | Solo HTTPS público con protección de red |
| Timeout | 3–60 segundos | 3–30 segundos |
| Lectura | URL, headers y payload completos | Sin secretos, valores de headers ni cuerpos completos |
Los endpoints creados o asumidos mediante una acción MCP real aplicable conservan la protección para envíos posteriores, incluidos los de la interfaz. Un PUT ordinario no la elimina. Lectura, dry-run, desactivación o borrado no transfieren por sí solos un endpoint antiguo a la nueva política.
Fuentes del contrato
Para mantenimiento: modelo y rutas web/routes/api/webhooks.py; eventos, firma y reintentos web/webhookdispatcher.py; datos en vivo functions/functions/spampm/crmintegration.py; transporte protegido web/webhooknetwork.py; MCP web/mcp/webhooks.py. Son rutas del repositorio para mantenimiento; configurar webhooks no requiere acceder al código.