公开 API 与 Webhook
REST API 让您的 CRM、网站后端或自有机器人能够像面板一样操作 cenaly.com 的线索和对话:读取卡片、修改属性、发送事件、在对话中回复。Webhook 则相反,由它主动通知您的服务器有新的线索、事件和消息。
启动所需条件#
| 项目 | 位置 |
|---|---|
| 角色 | 账户所有者——密钥以其名义生效;员工看不到密钥 |
| 板块 | 已启用的聊天(线索和对话都在其中) |
| 密钥 | 集成 → API 与 Webhook → 密钥 → 创建密钥 |
| API 地址 | https://api.cenaly.com/public-api/v1 |
| 方法参考 | api.cenaly.com/public-api/v1/docs |
| OpenAPI 3.1 | api.cenaly.com/public-api/v1/openapi.json |
第 1 步:密钥与权限#
- 打开集成,点击页面顶部的 API 与 Webhook。
- 在密钥选项卡中点击创建密钥,输入名称(“CRM”、“网站后端”、“Telegram 机器人”)。
- 选择权限集或逐项勾选权限,若有多个门店——选择密钥可访问的门店,以及有效期。
- 复制密钥
cnk_…。它只显示一次:我们仅保存其指纹(SHA-256)。
| 权限 | 作用 |
|---|---|
contacts:read |
线索列表、搜索和卡片、属性 |
contacts:write |
修改属性和标签、合并、按数据主体请求删除 |
events:read |
线索事件流 |
events:write |
发送事件(购买、注册、任意自定义事件) |
conversations:read |
对话和消息 |
conversations:write |
客服回复、备注、状态、负责人、标记 |
messages:send |
向线索的最近对话发送消息 |
segments:read |
已保存的细分及其成员 |
权限集:CRM / 后端(线索、属性、事件、读取对话和细分)、自有机器人(读取线索、在对话中回复、向线索写入)、只读、全部。
- 权限可即时修改密钥的权限和门店——集成继续正常工作。
- 重新签发会生成新密钥;旧密钥立即失效,权限和日志保留。
- 撤销——立即且永久生效,下一次请求将收到
401。
密钥通过 Authorization: Bearer cnk_… 请求头传递(或 X-Api-Key: cnk_…)。请仅在服务器上保存——不要放在浏览器或移动应用中。
第 2 步:首次请求#
KEY=cnk_ваш_ключ
API=https://api.cenaly.com/public-api/v1
# 密钥能做什么:权限、门店、限额
curl -H "Authorization: Bearer $KEY" "$API/me"
# 线索,最新的在前(每页 20 条)
curl -H "Authorization: Bearer $KEY" "$API/contacts?limit=20"
# 按姓名、邮箱或电话搜索
curl -H "Authorization: Bearer $KEY" "$API/contacts?q=anna"
响应始终是信封格式:
{
"meta": { "status": 200, "requestId": "…", "nextCursor": "WyIyMDI2LTEwLTA0…" },
"data": { "total": 128, "contacts": [ { "id": "…", "status": "lead", "name": "Анна", "updatedAt": "2026-10-04T09:12:00Z" } ] }
}
下一页——同一请求加上 cursor=<meta.nextCursor>;没有 nextCursor——就没有更多页了。错误——{"meta": {...}, "error": {"code": "…"}}。
门店。 如果密钥只能访问一个门店,无需指定。如果有多个——请添加 location=<门店 id>(列表——GET /v1/locations),否则响应为 400 location_required。
按您的 user_id 操作属性和事件。 不必保存我们的线索 id:添加 ?by=user_id,地址就变成您的用户标识符。若该 user_id 的线索尚不存在——我们会创建它。
# 属性:操作 update_or_create、set_once、add、delete、append、union、exclude
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"ops":[{"op":"update_or_create","key":"$email","value":"anna@example.com"},
{"op":"add","key":"orders","value":1},
{"op":"union","key":"interests","value":"yoga"}]}' \
"$API/contacts/u-1001/props?by=user_id"
# 批量事件(最多 100 条),相同 id 重复提交只计一次
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"events":[{"id":"ord-5531","userId":"u-1001","name":"order_paid","props":{"sum":45,"currency":"GEL"}}]}' \
"$API/events"
# 在对话中回复并更改状态
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"text":"Здравствуйте! Заказ передан курьеру."}' "$API/conversations/<id>/reply"
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"status":"resolved"}' "$API/conversations/<id>/status"
写入经过与面板编辑相同的队列:变更在数秒内生效,按 user_id 写入的响应为 202 并带 pending。在线索卡片中,修改者显示为“API: <密钥名称>”。
方法#
| 方法 | 作用 | 权限 |
|---|---|---|
GET /v1/me, GET /v1/locations |
密钥、权限、门店 | — |
GET /v1/contacts |
线索:status(anon、lead、user)、q、limit、cursor |
contacts:read |
GET /v1/contacts/{id} |
卡片:属性、标签、事件摘要 | contacts:read |
POST /v1/contacts/{id}/props |
以操作方式更新属性(最多 250 个)或 {"props":{…}} |
contacts:write |
POST / DELETE /v1/contacts/{id}/tags |
线索标签 | contacts:write |
POST /v1/contacts/merge |
合并两张卡片:先预览,然后 "apply": true |
contacts:write |
POST /v1/contacts/{id}/erase |
应数据主体请求删除("confirm": true) |
contacts:write |
GET /v1/contacts/{id}/events |
线索事件流 | events:read |
POST /v1/contacts/{id}/events, POST /v1/events |
事件(每次最多 100 个) | events:write |
GET /v1/contacts/{id}/conversations |
线索的对话 | conversations:read |
POST /v1/contacts/{id}/messages |
在最近一次对话中给线索发消息 | messages:send |
GET /v1/conversations, /{id}, /{id}/messages |
对话和消息 | conversations:read |
POST /v1/conversations/{id}/reply |
客服回复或备注("type": "note") |
conversations:write |
POST /v1/conversations/{id}/status, /assign, /tags |
状态、负责人、标记 | conversations:write |
GET /v1/segments, GET /v1/segments/{id}/contacts |
分群及其成员 | segments:read |
每个方法的字段、示例和错误码见参考文档。
第 3 步. 网络钩子#
网络钩子在门店中配置:聊天 → 设置 → 网络钩子 — https 地址、事件和签名密钥。除对话事件(新对话、新消息、状态、评价、标记)外,新增了线索事件:
| 事件 | 触发时机 |
|---|---|
lead.created |
出现新线索(联系人在聊天、表单中留下,或通过 API 创建) |
lead.props_changed |
线索属性发生变化 — 变更的键列表在 changedProps 中 |
lead.event_tracked |
线索发生事件;默认发送您的全部事件,不含系统事件($…),名称列表由“发送哪些事件”字段指定 |
message.goal_reached |
线索达成消息目标(弹窗、群发) |
{
"event": "lead.event_tracked",
"deliveryId": "WD…",
"at": "2026-10-04T09:12:03Z",
"webhookId": "…",
"locationId": "…",
"contact": { "id": "…", "status": "user", "userId": "u-1001", "props": { "orders": 3 }, "tags": ["vip"] },
"leadEvent": { "name": "order_paid", "at": "2026-10-04T09:12:00Z", "props": { "sum": 45 } }
}
只有当网络钩子勾选“传递访客联系方式”时,线索的姓名、邮箱和电话才会出现在请求体中。签名校验 — 用网络钩子密钥对请求体做 HMAC-SHA256:
# заголовок X-Meni-Signature: sha256=<hex>
echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET"
在 5 秒内返回 2xx — 否则会重试投递;连续多次失败后网络钩子会被停用,这在设置中可见。重复投递请按 deliveryId 去重。
限制#
- 每个密钥每分钟 60 个请求;超出则返回
429并带Retry-After响应头。剩余额度在X-RateLimit-*中。 - 每页最多 200 条记录(默认 50),每批最多 100 个事件,每个请求最多 250 个属性操作,每次最多 30 个标签。
- 每个账户最多 20 个有效密钥。
- 密钥调用日志保留 30 天:时间、方法、路径模板(不含 id 和请求体)、响应码、耗时。
- 通过 API 发送的事件会进入线索卡片(事件流、摘要、分群),但不会进入原始访问分析日志。
- 只能向已存在的对话给线索发消息:没有对话则返回
409 no_conversation。
故障排查#
| 响应 | 含义 | 处理方式 |
|---|---|---|
401 api_key_missing / api_key_invalid |
请求中没有密钥,或密钥错误、已撤销、已过期 | 检查请求头;必要时重新签发密钥 |
403 scope_required |
密钥缺少所需权限 | 在密钥的权限中添加权限 |
400 location_required |
密钥可访问多个门店 | 添加 location=<id> |
404 location_not_found |
该门店不在密钥的门店中 | 对照 GET /v1/locations 核对 |
404 not_found |
线索或对话不存在 | 对于您自己的 id,请使用 ?by=user_id |
429 rate_limited |
超过每分钟 60 个请求 | 等待 Retry-After 秒,将事件分批发送 |
502 upstream_error |
临时故障 | 稍后重试请求 |
每个请求都可在调用日志标签页中查看 — 含响应码和密钥名称。
常见问题#
可以从浏览器调用 API 吗? 不可以:密钥可访问门店的全部线索。请从自己的服务器调用 API。
user_id 与线索 id 有什么区别? 线索 id 是卡片在我们系统中的标识。user_id 是您系统中的用户标识(CRM 中的编号、网站上的账户 id);用 ?by=user_id 可以通过它定位卡片。
重复发送事件会怎样? 具有相同 id 的事件只计一次 — 网络出错时可以放心重试请求。
每个门店需要单独的密钥吗? 不需要:一个密钥可以操作多个门店,门店通过 location 参数选择。
相关章节#
- 访客聊天 — 对话、线索和网络钩子设置
- 工单 — 统一接入流
- 自有 PBX 的 API 和 MCP — 单独的电话通信 API
- 集成