Visão geral do módulo de webhooks
Contrato conferido no código em 24 de outubro de 2026. Descreve recursos implementados, não planos futuros. Para configurar pela interface, consulte o guia do usuário.
Duas direções das requisições
| Direção | Quem chama | Endereço | Finalidade |
|---|---|---|---|
| API local de gerenciamento | Seu script local → Deskgram | http://127.0.0.1:5000/api/webhooks | Configurar endpoints e consultar entregas |
| Webhook de saída | Deskgram → seu receptor | URL salva no endpoint | Enviar eventos por POST com JSON |
Host e porta são exemplos do backend local. Use os da sua instalação. Não é a API de licenças, o servidor MCP nem os métodos setWebhook/getUpdates da Telegram Bot API.
Estas rotas REST não fazem uma verificação Bearer separada. Use um ambiente local confiável; não exponha a porta do backend à internet para enviar webhooks. A entrega de saída não exige acesso de entrada ao computador. Uma camada de autorização própria, se houver, possui regras independentes.
As respostas locais são JSON. Use Content-Type: application/json ao enviar configurações por POST/PUT.
Início rápido pela API
Os comandos curl usam um shell que aceita aspas simples. No PowerShell, use curl.exe e as regras da sua versão para parâmetros JSON. Domínios, IDs e segredos são fictícios. O texto russo nos exemplos é um valor ilustrativo, não uma exigência do contrato.
- Crie um endpoint desativado para revisar antes da 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}'- Obtenha endpoint.id na resposta. Para o ID fictício 12, envie um teste:
curl -X POST 'http://127.0.0.1:5000/api/webhooks/endpoints/12/test'- Verifique success, delivery.responsestatuscode, delivery.response_body e o recebimento real. O teste não contém um lead real e pode falhar na validação comercial do CRM.
- Ative pela interface ou envie a configuração completa por PUT com enabled: true. PUT não é atualização parcial.
- Execute uma ação real correspondente ao evento e consulte o histórico:
curl 'http://127.0.0.1:5000/api/webhooks/deliveries?endpoint_id=12&limit=20'Objeto endpoint
Campos de criação e atualização
POST e PUT usam o mesmo modelo.
| Campo | Tipo | Padrão | Regras |
|---|---|---|---|
name | string | Obrigatório | 1–120 caracteres antes de remover espaços nas pontas; use nome não vazio |
url | string | Obrigatório | 8–2000 caracteres, começando por http:// ou https://; HTTPS recomendado |
method | string | POST | Apenas POST |
secret | string ou null | null na criação | Espaços nas pontas removidos; omitir no PUT preserva, null ou vazio apaga |
headers | object de valores string | {} | Cabeçalhos extras do receptor, não credenciais do Deskgram |
events | array de string | ["task.completed"] | Seis eventos do catálogo ou "*"; lista vazia vira task.completed |
module_filter | array de string | [] | Vazio significa todos os módulos; exemplo ["spam_pm"] |
payload_template | string | deskgram | deskgram, shortresult, crmlead ou custom |
payload_mapping | object | {} | Configuração dos campos descrita abaixo |
enabled | boolean | true | Permite entregas automáticas dos eventos correspondentes |
timeout_seconds | number | 15 | De 3 a 60 segundos |
Para endpoints comuns, timeout se aplica às operações de rede do cliente HTTP, não ao tempo total estrito da chamada local. Test e retry aguardam a entrega, incluindo tentativas e pausas.
Nomes de módulos passam para minúsculas e trocam - por _. Eventos e módulos duplicados são removidos. "*" cobre os eventos que realmente chegam ao mecanismo; não cria integrações ausentes em outros módulos.
Campos da resposta
Além das configurações, retornam id integer, hassecret boolean, createdat e updated_at em UTC. secret sempre retorna null, mesmo quando existe um segredo salvo.
A API REST local retorna os valores de headers sem mascarar. Eles podem conter tokens do receptor; não publique respostas completas em logs ou chamados de suporte.
Exemplo de criação:
{ "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 da API
Todos os caminhos incluem /api. Uma operação concluída retorna HTTP 200, inclusive criação e exclusão. endpointid e deliveryid são IDs inteiros dos registros.
GET /api/webhooks/endpoints
Lista do mais novo para o mais antigo por criação, sem parâmetros de paginação. Resposta: {"endpoints": [...]} com objetos endpoint; coleção vazia: {"endpoints": []}.
POST /api/webhooks/endpoints
Cria usando o modelo acima. Resposta: {"success": true, "endpoint": {...}}.
Se enabled for omitido, fica ativado imediatamente. Informe false durante a configuração inicial.
PUT /api/webhooks/endpoints/{endpoint_id}
Atualiza o endpoint. Exige name, url e todas as configurações atuais que devem permanecer. Campos omitidos voltam ao padrão, exceto secret.
Não reenvie o objeto de resposta sem tratar: secret: null apagaria o segredo salvo. Remova a chave secret para preservar. Null ou string vazia explícitos apagam.
Exemplo de ativação preservando o segredo:
{ "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}Resposta: {"success": true, "endpoint": {...}}. Endpoint inexistente: HTTP 404.
DELETE /api/webhooks/endpoints/{endpoint_id}
Exclui sem corpo de requisição. Resposta: {"success": true, "endpoint_id": 12}. Se não existir, HTTP 404.
O histórico permanece. Excluir não cancela um envio já iniciado. Não é possível repetir entregas depois de excluir o endpoint.
POST /api/webhooks/endpoints/{endpoint_id}/test
Envia um teste apenas ao endpoint indicado. Não exige corpo e não aceita um payload de teste arbitrário.
Funciona também desativado, sem aplicar assinatura de eventos ou filtro de módulos. Aplica o mapping atual e responde depois do envio e das tentativas automáticas.
Exemplo de rejeição HTTP 403 pelo receptor no formato padrão:
{ "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: рассылка"} } }}O status da API local continua HTTP 200. O 403 pertence ao receptor e fica dentro de delivery. Sucesso retorna success: true, delivered: 1, status: "success".
Se nenhuma entrega for criada, delivery_id e delivery podem ser null, com success false. Endpoint inexistente retorna HTTP 404 local.
GET /api/webhooks/deliveries
Lista entregas novas primeiro, por data de criação.
| Query | Tipo | Padrão | Significado |
|---|---|---|---|
endpoint_id | integer | Não informado | ID do endpoint |
event_type | string | Não informado | Nome exato, como message.failed |
status | string | Não informado | pending, retrying, success ou error |
limit | integer | 100 | Entre 1 e 500 registros |
Resposta: {"deliveries": [...]}. Cada item é uma entrega sem requestpayload e sourcepayload. Não há offset, cursor nem contagem total.
GET /api/webhooks/deliveries/{delivery_id}
Retorna detalhes com requestpayload e sourcepayload: {"delivery": {...}}. Entrega inexistente: HTTP 404.
POST /api/webhooks/deliveries/{delivery_id}/retry
Repete manualmente uma entrega em error. Não exige corpo. Cria novo registro e aguarda suas tentativas. Permitido também com endpoint desativado.
A resposta contém success boolean, originaldeliveryid integer, status string e delivery completo da nova entrega. Seu id e X-Deskgram-Delivery-Id são diferentes; a original permanece. Falha externa retorna HTTP 200 local com success: false.
HTTP 400: origem não está em error ou sua repetição manual já está em execução neste processo. HTTP 404: entrega ou endpoint não encontrado.
GET /api/webhooks/events
Catálogo dos seis eventos com rótulos. O exemplo mantém os rótulos russos efetivamente retornados; traduzir o documento não muda valores da API.
{ "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 é enviado pelo método de teste e não integra o catálogo de assinaturas. "*" é aceito nas configurações, mas também não aparece no catálogo.
Objeto delivery
| Campo | Tipo | Descrição |
|---|---|---|
id | integer | ID na base local |
endpoint_id | integer | ID do endpoint, mesmo excluído depois |
event_type | string | Evento independente do mapping de saída |
task_id | string ou null | ID de tarefa quando disponível |
status | string | pending, retrying, success ou error |
attempts | integer | Tentativas automáticas realizadas |
responsestatuscode | integer ou null | Último HTTP; pode ser null na falha de rede |
response_body | string ou null | Última resposta truncada; pode ser null na falha de rede |
error | string ou null | Diagnóstico, não campo comercial de erro do CRM |
createdat, updatedat | string | Datas UTC ISO 8601 com Z |
delivered_at | string ou null | Horário de entrega bem-sucedida |
request_payload | object | JSON após mapping; apenas detalhes/test/retry |
source_payload | object | Evento original; pode ser {} em registros antigos; apenas detalhes/test/retry |
No transporte comum, a resposta fica limitada a 2000 caracteres; o transporte protegido MCP também limita bytes lidos. O histórico não é uma captura HTTP completa: não guarda cabeçalhos enviados nem todas as respostas intermediárias.
Eventos e JSON de saída
Exemplos para payload_template: "deskgram". O mapping altera a estrutura externa; X-Deskgram-Event mantém o nome do evento.
Eventos finais de tarefas
| Evento | Significado |
|---|---|
task.completed | Execução encerrada, sem garantir sucesso de cada ação |
task.failed | Estado final de erro |
task.cancelled | Tarefa parada ou 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 e result variam por módulo, sem um esquema universal de contadores. taskname, modulename e created_at podem ser null. Failed/cancelled usam a mesma família estrutural, mudando evento, estado e resultados disponíveis.
Eventos de mensagens em tempo real
message.sent, message.failed e lead.replyreceived estão atualmente ligados a spampm. Filtrar outro módulo não cria novas fontes.
Exemplo de envio:
{ "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 e message dependem dos dados disponíveis. Podem faltar telefone, username, nome, ID ou texto. Para texto, use message.text. message_text é adicional de um caminho específico, não universal.
message.failed contém error e estado disponíveis em message; um ID enviado pode não existir. Destinatários ignorados ou falhas de resolução nem sempre geram esse evento.
lead.replyreceived pode incluir messageid, text, media_type, datetime, timestamp e status: "received". Só surge quando um manipulador ativo realmente recebe a resposta. O webhook não ativa escuta contínua nem garante coleta após o fim da tarefa.
message.sent não confirma leitura. Trate IDs como valores opacos: tarefa, conta e usuário Telegram são strings; um message_id específico pode ser numérico.
Evento de teste
Contém schemaversion, event: "webhook.test", occurredat, source, test: true e endpoint: {id, name}. O exemplo do método test mostra tudo.
Não há task, lead, account ou message. Mapping apenas com fontes de lead produz {}.
Mapeamento de campos
O formato deskgram envia o evento original completo e ignora payload_mapping. Escolha outro para limitar dados.
Para shortresult, crmlead e custom, recomenda-se a forma explícita:
{ "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} ]}Coloque o objeto inteiro em payload_mapping. target é o caminho no receptor; source, no evento original. Não são constantes nem expressões executáveis.
Resultado do exemplo message.sent:
{ "event": "message.sent", "contact": {"telegram_id": "123456789", "username": "example_client"}, "comment": "Здравствуйте!", "status": "sent"}Regras:
- Pontos em target criam objetos aninhados. Evite conflitos como contact e contact.username juntos.
- source lê chaves de objetos separadas por ponto. Não há índices de arrays nem modelos
{{...}}. - Ausentes e null são omitidos. Fonte única preserva 0, false e string vazia com seu tipo JSON.
- Lista de fontes escolhe o primeiro valor diferente de null, string vazia, lista vazia ou objeto vazio. Também aceita
"message.status|task.status". - enabled: false exclui; omitir enabled inclui.
{"fields": []}ou todos desativados produzem{}, sem fallback para payload completo.- payloadmapping: {} usa campos internos em shortresult/crm_lead; custom produz {}. fields explícitos fixam o contrato.
shortresult interno usa os disponíveis event, occurredat, taskid, module, status, lead, message, error. crmlead interno usa event, occurredat, taskid, accountid, telegramuserid, username, phone, firstname, lastname, rawidentifier, messageid, messagetext, messagestatus, messageerror.
A interface pode salvar sua própria lista editável de fields do preset. A configuração salva determina o contrato real, não apenas o nome do formato.
Cabeçalhos de saída
| Cabeçalho | Valor |
|---|---|
Content-Type | application/json |
User-Agent | Deskgram-Webhooks/1.0 |
X-Deskgram-Delivery-Id | ID da entrega como string |
X-Deskgram-Event | Nome do evento |
x-secret | Segredo compartilhado, quando configurado |
X-Deskgram-Webhook-Secret | O mesmo segredo |
X-Deskgram-Signature | Assinatura sha256=, quando há segredo |
O segredo é chave HMAC e também viaja em dois cabeçalhos. Use HTTPS e não publique esses valores nos logs.
headers não pode sobrescrever Content-Type, User-Agent, Delivery-Id, Event nem, com segredo definido, os três cabeçalhos ligados a ele, independentemente de maiúsculas. Outros, como Authorization do receptor, são adicionais.
Verificação da assinatura
Algoritmo: HMAC-SHA256(secretutf8, rawrequest_body). O cabeçalho traz sha256= seguido de digest hexadecimal minúsculo.
A assinatura usa os bytes exatos enviados. JSON é UTF-8, sem escape ASCII de texto não ASCII, chaves ordenadas e separadores compactos. Use o corpo HTTP bruto, não JSON analisado e serializado novamente.
Python usando só a biblioteca padrão:
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 FalsePasse bytes brutos, X-Deskgram-Signature e segredo salvo no receptor. Na falha, não execute a ação comercial. Carregue o segredo da configuração protegida, não confie no x-secret recebido.
A assinatura não impede sozinha a repetição de um corpo assinado. Idade e deduplicação exigem lógica no receptor. Delivery-Id não integra o corpo assinado. O segredo é aparado: use ASCII não vazio, sem espaços nas pontas e até 1000 caracteres.
Tentativas e garantias
- Sucesso somente HTTP 200–299. Não segue 3xx. HTTP 200 com erro comercial no JSON ainda é sucesso de transporte.
- Até três tentativas, com pausas aproximadas de 2 e 4 segundos antes da segunda e terceira. Repete falhas de rede e HTTP, inclusive 4xx. Retry-After não recebe tratamento separado.
- Tentativas automáticas mantêm corpo e Delivery-Id. O receptor deve aceitar repetição sem repetir a ação concluída.
- Retry manual usa URL, segredo e headers atuais. Com sourcepayload, reaplica mapping atual ao evento original; registros antigos sem ele repetem requestpayload sem novo mapping.
- O manual cria novo Delivery-Id. Evite duplicatas comerciais também por tarefa/conta/mensagem e tipo de evento, conforme seu fluxo.
- Delivery-Id é único numa base local, não entre todas as instalações. Diferencie remetentes quando houver vários.
- Não garante exactly-once, ordem entre eventos nem retomada automática após parar o processo. Pending/retrying interrompido não implica continuação ao reiniciar.
- Desativar impede novos eventos automáticos, não cancela envios iniciados. Test/retry explícitos podem enviar desativado.
Fluxo recomendado: validar assinatura, salvar ou enfileirar de forma confiável, responder 2xx e processar trabalhos longos separadamente.
Erros da API
| HTTP local | Quando |
|---|---|
| 200 | Operação concluída; verificar success/delivery em test/retry |
| 400 | Método/formato/evento não suportado, URL sem http(s), retry inválido |
| 404 | Endpoint/entrega ausentes ou endpoint original excluído |
| 422 | Validação de corpo, tipos ou intervalo de limit/timeout |
Exemplo; o texto real do servidor pode permanecer em russo:
{"detail": "Webhook endpoint не найден"}Em validação, detail pode ser array diagnóstico do FastAPI, não string. Falha de banco ou backend também pode causar erro de servidor, separado da resposta externa do CRM.
Diferenças do MCP
MCP é uma interface separada para agente IA, não sinônimo desta REST API. Veja a conexão MCP.
| Propriedade | REST local | Ferramentas MCP |
|---|---|---|
| Novo endpoint | enabled=true por padrão | Desativado |
| Prévia | Executa a requisição | Criar/alterar usa dry-run por padrão conforme ajustes MCP |
| Atualização | PUT completo, exceção secret | Parcial |
| URL | http(s); não exponha a API local | Apenas HTTPS público com proteção de rede |
| Timeout | 3–60 segundos | 3–30 segundos |
| Leitura | URL, valores headers e payload completos | Sem segredos, valores headers ou corpos completos |
Um endpoint criado ou assumido por ação MCP real aplicável mantém as regras protegidas nos próximos envios, inclusive pela interface. PUT comum não remove esse modo. Leitura, dry-run, desativar ou excluir não colocam por si só um endpoint antigo nessa política.
Fontes do contrato
Para manutenção: modelo e rotas web/routes/api/webhooks.py; eventos, assinatura e tentativas web/webhookdispatcher.py; dados ao vivo functions/functions/spampm/crmintegration.py; transporte protegido web/webhooknetwork.py; MCP web/mcp/webhooks.py. São caminhos do repositório para mantenedores; configurar não exige ler o código.