本文根据 2026 年 10 月 24 日的实现核对,介绍已经实现的行为,不代表未来计划。通过界面配置请参阅用户指南。
两种请求方向
| 方向 | 调用方与接收方 | 地址 | 用途 |
|---|---|---|---|
| 本地管理 API | 本地脚本 → Deskgram | http://127.0.0.1:5000/api/webhooks | 管理接收端点、查看投递历史 |
| 出站 Webhook | Deskgram → 您的接收服务 | 端点设置中的 URL | 通过 POST 发送 JSON 事件 |
127.0.0.1:5000 只是本地 backend 地址示例,请使用实际安装的地址和端口。它不是许可证云服务 API,也不是 MCP 服务器;同样不是 Telegram Bot API 的 setWebhook 或 getUpdates。
下列 REST 路由没有单独的 Bearer token 校验,适用于受信任的本地环境。不要为了接收出站通知而向互联网开放 backend 端口。出站投递不需要外部主动连接运行 Deskgram 的电脑。如果您另设了身份验证网关,其规则需要单独配置。
本地响应采用 JSON。通过 POST/PUT 提交设置时,发送 Content-Type: application/json。
API 快速开始
下面的 curl 示例适用于支持单引号的 shell。在 PowerShell 中请使用 curl.exe,并遵循当前版本处理 JSON 参数的规则。所有域名、ID 和密钥均为虚构示例。JSON 中的俄文字符串只是示例值,不是固定要求。
- 先创建未启用的端点,检查设置:
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}'- 从响应读取
endpoint.id。假设 ID 为12,发送测试:
curl -X POST 'http://127.0.0.1:5000/api/webhooks/endpoints/12/test'- 查看
success、delivery.responsestatuscode、delivery.response_body,并在接收服务检查实际请求。测试没有真实线索,可能不满足 CRM 的业务校验。 - 在界面启用端点,或通过 PUT 提交带有
enabled: true的完整设置。不要只发送 enabled,PUT 不是局部更新。 - 执行一次与订阅事件对应的实际操作,再查看历史:
curl 'http://127.0.0.1:5000/api/webhooks/deliveries?endpoint_id=12&limit=20'Endpoint 对象
创建与更新的请求字段
POST 和 PUT 使用同一个请求模型。
| 字段 | 类型 | 默认值 | 规则 |
|---|---|---|---|
name | string | 必填 | 去除首尾空格前为 1–120 个字符;请使用非空名称 |
url | string | 必填 | 8–2000 个字符,以 http:// 或 https:// 开头;建议 HTTPS |
method | string | POST | 仅支持 POST |
secret | string 或 null | 创建时为 null | 共享密钥,首尾空格被删除;PUT 省略则保留,null 或空字符串则清除 |
headers | 字符串值 object | {} | 接收服务所需的额外请求头,不是 Deskgram 访问凭据 |
events | string 数组 | ["task.completed"] | 事件目录中的六种事件或 "*";空列表会替换为 task.completed |
module_filter | string 数组 | [] | 空列表表示所有模块,例如 ["spam_pm"] |
payload_template | string | deskgram | deskgram、shortresult、crmlead、custom |
payload_mapping | object | {} | 输出字段配置,见下文 |
enabled | boolean | true | 是否自动投递匹配事件 |
timeout_seconds | number | 15 | 3–60 秒 |
普通端点的 timeout 用于 HTTP 客户端的网络操作,并不是整个本地 API 调用的严格总时长。test 和 retry 会等待投递完成,包括自动尝试及其间隔。
模块名转为小写,- 替换为 _;事件和模块列表会去重。"*" 指所有实际进入此机制的事件,并不会给尚未接入事件的模块增加功能。
响应字段
Endpoint 包含上述设置,以及整数 id、布尔 hassecret、UTC 时间 createdat 和 updated_at。即使已保存密钥,secret 也始终返回 null。
本地 REST API 不会隐藏 headers 中的值,其中可能有接收服务的 token。不要把完整响应放入公开日志或支持消息。
创建响应示例:
{ "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" }}API 方法
以下路径均包含 /api。管理操作正常完成时使用 HTTP 200,创建和删除也如此。endpointid 与 deliveryid 是对应记录的整数 ID。
GET /api/webhooks/endpoints
按创建时间从新到旧返回端点,无分页参数。响应为 {"endpoints": [...]},每项是一个 endpoint 对象;空列表为 {"endpoints": []}。
POST /api/webhooks/endpoints
使用上面的模型创建端点,返回 {"success": true, "endpoint": {...}}。
如果省略 enabled,新端点会立即启用。首次配置时可明确发送 false,在完成检查后再开启自动通知。
PUT /api/webhooks/endpoints/{endpoint_id}
更新端点。必须提供 name、url 和需要保留的其余当前设置。省略字段恢复默认值,只有 secret 例外。
不要直接把读取到的 endpoint 对象原样回传:响应中的 secret: null 会在 PUT 中清除已保存的密钥。保留旧密钥时应完全省略 secret 键;明确发送 null 或空字符串表示清除。
保留密钥并启用端点的示例:
{ "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}响应为 {"success": true, "endpoint": {...}}。端点不存在时返回 HTTP 404。
DELETE /api/webhooks/endpoints/{endpoint_id}
删除端点,无需请求体。返回 {"success": true, "endpoint_id": 12};不存在时为 HTTP 404。
投递历史仍保留。删除不会取消已经开始的发送;端点删除后,其历史投递无法重试。
POST /api/webhooks/endpoints/{endpoint_id}/test
只向指定端点发送测试,无需请求体。本方法不接受任意自定义测试 payload。
显式测试可以发送给未启用的端点,不受事件订阅和模块筛选限制。它会使用当前 mapping,并在发送及自动重试完成后返回。
下面是标准格式的测试被接收方以 HTTP 403 拒绝时的示例:
{ "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: рассылка"} } }}此时本地 API 的 HTTP 状态仍是 200。403 属于外部接收方,保存在 delivery 中。成功时为 success: true、delivered: 1、status: "success"。
如果没有创建投递记录,delivery_id 和 delivery 可以为 null,success 为 false。端点不存在则返回本地 HTTP 404。
GET /api/webhooks/deliveries
按创建时间从新到旧列出投递。
| 查询参数 | 类型 | 默认值 | 含义 |
|---|---|---|---|
endpoint_id | integer | 未指定 | 端点 ID |
event_type | string | 未指定 | 完整事件名,例如 message.failed |
status | string | 未指定 | pending、retrying、success 或 error |
limit | integer | 100 | 1–500 条记录 |
响应为 {"deliveries": [...]}。每项是不包含 requestpayload、sourcepayload 的投递对象。没有 offset、游标或总记录数。
GET /api/webhooks/deliveries/{delivery_id}
读取完整投递详情,包括 requestpayload、sourcepayload。返回 {"delivery": {...}};不存在时返回 HTTP 404。
POST /api/webhooks/deliveries/{delivery_id}/retry
手动重试状态为 error 的投递,无需请求体。它创建新投递记录并等待其尝试完成。即使端点已停用,也允许显式重试。
响应包含布尔 success、整数 originaldeliveryid、字符串 status,以及完整的新 delivery。
新投递的 id 和 X-Deskgram-Delivery-Id 与原记录不同;不会覆盖原记录。接收方失败时,本地仍返回 HTTP 200,并以 success: false 表示失败。
HTTP 400 表示原记录不是 error,或本进程已在执行该记录的手动重试。HTTP 404 表示投递或端点不存在。
GET /api/webhooks/events
返回六种支持的事件及界面标签。下面保留 API 实际返回的俄文 label;文档翻译不会改变接口值。
{ "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 由单独的测试方法发送,不在订阅目录中。"*" 可以写入设置,但目录也不返回它。
Delivery 对象
| 字段 | 类型 | 含义 |
|---|---|---|
id | integer | 本地数据库中的投递 ID |
endpoint_id | integer | 端点 ID,即使端点后来删除仍保留 |
event_type | string | 不受输出 mapping 改名影响的事件类型 |
task_id | string 或 null | 可用时的任务 ID |
status | string | pending、retrying、success 或 error |
attempts | integer | 已进行的自动尝试次数 |
responsestatuscode | integer 或 null | 最后 HTTP 状态码;网络错误时可能为 null |
response_body | string 或 null | 截短后的最后响应;网络错误时可能为 null |
error | string 或 null | 诊断信息,不是 CRM 业务错误字段 |
createdat、updatedat | string | 以 Z 结尾的 ISO 8601 UTC 时间 |
delivered_at | string 或 null | 成功投递时间 |
request_payload | object | mapping 后的 JSON;仅在详情/test/retry 中出现 |
source_payload | object | mapping 前的原始事件;旧记录可能是 {};仅在详情/test/retry 中出现 |
普通投递的响应文本最多保留 2000 个字符;MCP 受保护传输还限制读取的字节数。历史不是完整 HTTP 跟踪,不保存发出请求的所有头部,也不保存每次中间尝试的完整响应。
事件与出站 JSON
以下示例对应 payload_template: "deskgram"。使用 mapping 后,外部 JSON 结构会变化,但 X-Deskgram-Event 仍是事件名称。
任务最终事件
| 事件 | 含义 |
|---|---|
task.completed | 任务执行完成,不保证其中每个动作均成功 |
task.failed | 任务最终错误状态 |
task.cancelled | 任务停止或取消 |
{ "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、result 的内容取决于模块,没有适用于所有任务的统一计数器模式。taskname、modulename、created_at 可以为 null。failed/cancelled 使用同类结构,事件、状态和可用结果相应变化。
实时消息事件
目前 message.sent、message.failed、lead.replyreceived 接入的是 spampm。把筛选器换成另一个模块不会产生新的事件来源。
已发送消息的示例:
{ "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 和 message 的字段取决于可用数据。电话、username、姓名、ID 或文本都可能缺失。映射文本时用 message.text。不要把 message_text 作为通用必填项:它只是特定发送路径提供的额外字段。
message.failed 的 message 中包含可用的 error 和状态,不一定有成功发送的 message_id。跳过收件人或解析收件人失败,并不一定生成此事件。
lead.replyreceived 的 message 可包含 messageid、text、media_type、datetime、timestamp、status: "received"。只有正在运行的处理器实际收到回复时才产生事件。Webhook 本身不会开启全天账号监听,也不保证任务结束后仍收集回复。
message.sent 不表示收件人已读。建议把 ID 视为不透明标识;此契约中的任务、账号和 Telegram 用户 ID 是字符串,个别 message_id 则可能是数值。
测试事件
原始测试包含 schemaversion、event: "webhook.test"、occurredat、source、test: true、endpoint: {id, name}。完整结构可见 test 方法响应。
测试中没有 task、lead、account、message。只映射线索字段时会得到 {}。
字段映射
deskgram 发送完整原始事件,忽略 payload_mapping。需要限制传输数据时请选择其他模板。
对于 shortresult、crmlead、custom,推荐明确使用:
{ "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} ]}把整个对象放进 payload_mapping。target 是接收方字段路径,source 是 Deskgram 原始事件中的路径,不是常量或可执行表达式。
上述 message.sent 示例的映射结果:
{ "event": "message.sent", "contact": {"telegram_id": "123456789", "username": "example_client"}, "comment": "Здравствуйте!", "status": "sent"}规则如下:
- target 中的点创建嵌套对象,不要同时设置 contact 和 contact.username 之类冲突路径。
- source 按点读取嵌套对象键,不支持数组下标或
{{...}}模板。 - 缺失值或 null 被省略;单一来源的 0、false、空字符串保留原 JSON 类型。
- 来源列表采用第一个不是 null、空字符串、空列表、空对象的值;也支持
"message.status|task.status"。 - enabled: false 排除该字段,省略 enabled 表示启用。
{"fields": []}或全部禁用产生{},不会替换为完整 payload。- payloadmapping 为 {} 时,shortresult/crm_lead 使用内置字段,custom 则产生 {}。显式 fields 可固定输出契约。
内置 shortresult 使用可用的 event、occurredat、taskid、module、status、lead、message、error。内置 crmlead 包含 event、occurredat、taskid、accountid、telegramuserid、username、phone、firstname、lastname、rawidentifier、messageid、messagetext、messagestatus、messageerror。
界面可能为预设保存自己的可编辑 fields 列表。端点实际保存的设置决定输出,不应仅凭预设名称推断全部结构。
出站请求头
| 请求头 | 值 |
|---|---|
Content-Type | application/json |
User-Agent | Deskgram-Webhooks/1.0 |
X-Deskgram-Delivery-Id | 投递记录 ID 的字符串形式 |
X-Deskgram-Event | 事件名,例如 message.sent |
x-secret | 已配置的共享密钥 |
X-Deskgram-Webhook-Secret | 同一共享密钥 |
X-Deskgram-Signature | 配置密钥时的 sha256= 签名 |
密钥不只用于 HMAC,还会作为两个头部的值发送。请使用 HTTPS,并避免将这些头部写入公开日志。
无论大小写如何,headers 都不能覆盖系统 Content-Type、User-Agent、Delivery-Id、Event;配置密钥后也不能覆盖三个密钥相关头部。接收方 Authorization 等其他头部可以另外添加。
验证签名
算法为 HMAC-SHA256(secretutf8, rawrequest_body)。头部值由 sha256= 加小写十六进制摘要组成。
签名针对实际发送的准确字节。JSON 使用 UTF-8,不对非 ASCII 字符作 ASCII 转义,键排序且分隔符不带空格。接收方必须使用原始 HTTP body,不要先解析 JSON 再序列化。
以下 Python 示例只使用标准库:
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 False传入原始 body 字节、X-Deskgram-Signature 头部和接收方保存的密钥。验证失败时不要执行业务动作。密钥应从受保护配置加载,不能直接信任传入的 x-secret。
签名本身不能防止重放已签名请求;时间检查和去重需要接收方逻辑。Delivery-Id 不在签名 body 内。密钥会去掉首尾空格,建议使用不带首尾空格、长度不超过 1000 的非空 ASCII 密钥。
重试与保证范围
- 只有 HTTP 200–299 为成功。不会跟随 3xx。HTTP 200 中即使带业务错误 JSON,仍视为传输成功。
- 最多三次自动尝试,第二、三次前大约等待 2、4 秒;网络失败和 HTTP 拒绝(包括 4xx)都会重试,不单独处理 Retry-After。
- 自动尝试使用相同 body 和 Delivery-Id。接收方应保证重复请求不会重复已完成的业务动作。
- 手动 retry 使用当前 URL、secret、headers。存在 sourcepayload 时,对原始事件应用当前 mapping;旧记录缺少原事件时,直接重发已保存的 requestpayload,不二次映射。
- 手动重试创建新 Delivery-Id。业务去重还应使用适合场景的任务/账号/消息标识和事件类型。
- Delivery-Id 只在某个本地数据库中唯一,不是所有 Deskgram 安装的全局 ID。接收多个来源时应区分发送方。
- 不保证 exactly-once、不同事件顺序或进程重启后自动恢复队列。中断后的 pending/retrying 不代表下次启动会继续。
- 停用端点只阻止新的自动事件选择,不取消已开始的发送;显式 test/retry 仍可发送到停用端点。
建议接收流程:验证签名,可靠保存事件或写入自己的队列,返回 2xx,再单独执行耗时业务处理。
API 错误
| 本地 HTTP 状态 | 情况 |
|---|---|
| 200 | 操作完成;test/retry 还应检查 success 和 delivery |
| 400 | 不支持的方法/格式/事件、URL 无 http(s)、手动重试状态无效 |
| 404 | 端点或投递不存在,或原投递的端点已删除 |
| 422 | 请求体、参数类型、limit/timeout 范围验证失败 |
应用错误示例,实际服务器文本可能仍为俄文:
{"detail": "Webhook endpoint не найден"}验证失败时 detail 可能是 FastAPI 诊断数组,而不是字符串。数据库或 backend 故障还可能造成服务器错误,这不是外部 CRM 的响应。
与 MCP 的区别
MCP 是 AI 代理的独立管理接口,并不等同于此 REST API。连接方式见 MCP 概览。
| 属性 | 本地 REST API | MCP 工具 |
|---|---|---|
| 新端点 | 默认 enabled=true | 默认停用 |
| 预览模式 | 请求实际执行 | 创建/修改按 MCP 设置默认 dry-run |
| 更新 | 完整 PUT,secret 保留规则例外 | 局部更新 |
| 接收地址 | http(s),不要开放本地管理 API | 仅公共 HTTPS,使用网络保护策略 |
| Timeout 设置 | 3–60 秒 | 3–30 秒 |
| 读取内容 | 完整 URL、headers 值、payload 详情 | 不返回密钥、headers 值或完整投递 body |
通过适用的实际 MCP 操作创建或纳入管理的端点,在以后发送时仍保留网络保护规则,包括从界面发出的请求。普通 PUT 不会解除。只读、dry-run、停用或删除操作不会把旧端点转入新策略。
契约来源
维护参考:REST 模型和路由 web/routes/api/webhooks.py;事件/签名/重试 web/webhookdispatcher.py;实时数据 functions/functions/spampm/crmintegration.py;受保护传输 web/webhooknetwork.py;MCP web/mcp/webhooks.py。这些是维护人员的仓库路径,使用模块不需要读取源代码。