CRM 微信推送接口调用文档
更新日期:2026-10-26。接口版本:v1。
CRM 提交客户信息后,服务将其映射为“仙宝科技”公众号的“新客户跟进通知”,持久化入队,再异步发送。CRM 无需管理公众号 AppSecret、微信 access_token、模板 ID 或微信字段名。
- 服务地址:
https://openid.xianboss.com - 在线文档:CRM 推送接口
- OpenAPI 3.0.3:下载 JSON,可导入 Apifox、Postman 等 API 工具。
- 完整请求 JSON 示例见下方“提交 CRM 推送”与“cURL 示例”。
1. 鉴权
每个调用系统使用独立的 API Key + API Secret,两项必须同时提供:
X-API-Key: YOUR_CRM_API_KEY
X-API-Secret: YOUR_CRM_API_SECRET
Key 也可通过 Bearer 提供,此时仍须携带 Secret:
Authorization: Bearer YOUR_CRM_API_KEY
X-API-Secret: YOUR_CRM_API_SECRET
请求头同时包含 Content-Type: application/json。密钥在单独的接入信息文件中交付,本文和公开在线文档不包含真实密钥。请在 CRM 服务端读取环境变量或安全配置,避免将业务密钥写入浏览器前端。调用系统须获授权使用 xianbao 公众号通道,且接收人 OpenID 必须已由管理员绑定到该系统及该通道。管理员密钥用于后台管理。
openId 指通知接收人,例如负责跟进的 CRM 员工或管理人员的微信身份;customerName 等客户字段来自 CRM 客户记录,两者不必是同一个人。CRM 可以将员工账号与其 OpenID 绑定,再按负责人发送通知。
接收人的 OpenID 必须属于目标公众号,接收人需已关注公众号并符合微信的消息接收条件。用户可在微信内打开 OpenID 获取页面 后获取自己的 OpenID。
系统与接收人隔离
管理员在管理后台为每个调用系统创建凭据、选择允许使用的通道,并添加该系统的接收人。接收人授权按“调用系统 + 公众号通道 + OpenID”精确匹配,OpenID 区分大小写。新系统未绑定接收人时不能推送;拥有相同通道权限的其他系统不能使用未分配给自己的 OpenID。
获取 OpenID 或在 CRM 内绑定员工不会自动授予 PushMsg 发送权限,仍需管理员在对应系统下添加接收人。未授权、停用或删除的接收人返回 403 RECIPIENT_FORBIDDEN,不会入队;重复提交也会重新检查权限。队列发送前再次检查系统、通道和接收人权限,撤销后尚未开始发送的任务会被阻止。
创建或轮换凭据时,Secret 仅返回一次,服务端保存其摘要;Key 可在后台查看。请将二者保存在调用系统服务端配置中。轮换会同时更换 Key 和 Secret,旧凭据立即失效,调用系统 ID、历史消息及幂等范围保留。旧版只有 Key 的接入必须由管理员轮换凭据并配置接收人后,改为双凭据调用。
2. 提交 CRM 推送
POST /api/v1/crm/push
Content-Type: application/json
X-API-Key: YOUR_CRM_API_KEY
X-API-Secret: YOUR_CRM_API_SECRET
{
"requestId": "crm-customer-C0001-followup-20261026-001",
"openId": "YOUR_OPENID",
"customerName": "测试客户",
"customerType": "潜在客户",
"customerSource": "CRM录入",
"customerPhone": "13800000000",
"markedAt": "2026-10-26 11:00",
"url": "https://crm.example.com/customers/C0001"
}
| 字段 | 必填 | 类型与限制 | 用途 |
|---|---|---|---|
| requestId | 是 | 非空字符串,最长 128 字符 | 同一调用方中的业务通知唯一标识,用于幂等去重 |
| openId | 是 | 非空字符串,最长 128 字符 | 通知接收人(如 CRM 负责人)在“仙宝科技”公众号下的 OpenID |
| customerName | 是 | 非空字符串,最长 20 字符 | CRM 客户记录中的客户名称 |
| customerType | 是 | 非空字符串,最长 20 字符 | 客户类型 |
| customerSource | 是 | 非空字符串,最长 20 字符 | 客户来源 |
| customerPhone | 是 | 3–17 字符,至少一个数字;只允许数字、+、括号、空格和短横线 |
客户联系电话 |
| markedAt | 是 | 严格格式 yyyy-MM-dd HH:mm,且必须是有效日期时间 |
标记时间,按北京时间解释 |
| url | 否 | 完整 HTTP/HTTPS URL,最长 2048 字符;可省略或传 null | 点击消息后的 CRM 详情地址 |
必填字段不接受全空白内容,所有字符串字段不接受控制字符。接口不自动去除空格或改写字段值。markedAt 不会默认取当前时间,CRM 应提供本次业务事件的固定时间,便于重试时保持请求一致。
请求只允许上述字段。额外提交 channelId、templateId、type、data 等字段会返回 400 UNSUPPORTED_CRM_FIELD。公众号与模板由服务端配置。
2.1 接收成功
首次提交返回 HTTP 202,表示已接收并入队:
{
"id": "11111111-2222-4333-8444-555555555555",
"status": "PENDING",
"duplicate": false
}
保存返回的 id,使用查询接口跟踪后续结果。HTTP 202 不表示微信已接受消息。
2.2 幂等与网络重试
幂等范围是“调用方 API Key 对应的应用 + requestId”。请求标识区分大小写。
- 同一
requestId、相同映射内容:返回 HTTP 200、原消息id、当前status和duplicate:true,不会创建第二条任务。 - 同一
requestId、不同映射内容:返回 HTTP 409、IDEMPOTENCY_CONFLICT。 - CRM 遇到网络超时、连接中断或临时服务错误时,复用原 requestId 和相同业务内容再次提交。不要通过换一个 requestId 的方式重试同一通知。
- 新的业务通知应使用新的 requestId,例如
crm-customer-C0001-followup-事件编号。
JSON 对象的键顺序不会影响去重;字段值、接收人和详情 URL 改变会影响消息内容。保持重试请求完全一致最容易接入。
3. 查询发送状态
GET /api/v1/crm/messages/{id}
X-API-Key: YOUR_CRM_API_KEY
X-API-Secret: YOUR_CRM_API_SECRET
必须使用原业务调用方的密钥。其他调用方的消息按不存在处理,返回 HTTP 404。
{
"id": "11111111-2222-4333-8444-555555555555",
"requestId": "crm-customer-C0001-followup-20261026-001",
"status": "SENT",
"attempts": 1,
"maxAttempts": 5,
"wechatMessageId": "WECHAT_MESSAGE_ID",
"lastError": null,
"createdAt": "2026-10-26T11:00:00",
"updatedAt": "2026-10-26T11:00:01"
}
| 字段 | 说明 |
|---|---|
| id / requestId | 消息 ID / CRM 业务请求标识 |
| status | 下表中的队列状态 |
| attempts / maxAttempts | 当前发送尝试次数 / 自动重试上限 |
| wechatMessageId | 微信返回的消息 ID,以字符串处理;尚未成功或未返回时为 null |
| lastError | 脱敏后的错误说明,正常时为 null |
| createdAt / updatedAt | 北京时间,不含时区后缀,可能带小数秒 |
| status | 含义 | CRM 处理建议 |
|---|---|---|
| PENDING | 等待发送 | 稍后查询 |
| PROCESSING | 正在处理 | 稍后查询,不另建任务 |
| RETRY | 等待自动重试 | 稍后查询,不由 CRM 创建重复任务 |
| SENT | 微信接口已接受 | 记录结果;这不代表用户已收到或阅读 |
| FAILED | 微信明确拒绝、配置错误或达到重试上限 | 查看 lastError,由管理员处理 |
| UNKNOWN | 发送结果无法确定 | 人工核实;不要自动重发或换新 requestId |
可每 2–5 秒查询一次;业务通知量较大时适当放宽查询间隔。服务仅对明确可重试的临时错误进行退避重试,最多 5 次。UNKNOWN 的人工重试可能重复通知,需要先确认实际发送情况。
4. 查看 CRM 模板配置
GET /api/v1/crm/template
X-API-Key: YOUR_CRM_API_KEY
X-API-Secret: YOUR_CRM_API_SECRET
接口要求业务密钥具备当前通道权限。当前配置如下:
{
"channelId": "xianbao",
"templateName": "新客户跟进通知",
"templateId": "OpH0RvriM-Zi5WmN25X6ox0FY6rKMoStoXb_ab-jiRw",
"fields": [
{"field": "customerName", "templateField": "thing2", "label": "客户名称"},
{"field": "customerType", "templateField": "thing3", "label": "客户类型"},
{"field": "customerSource", "templateField": "thing5", "label": "客户来源"},
{"field": "customerPhone", "templateField": "phone_number6", "label": "客户电话"},
{"field": "markedAt", "templateField": "time8", "label": "标记时间"}
],
"timeZone": "Asia/Shanghai"
}
CRM 发送时只传业务字段,不需要将这里的模板 ID 或微信字段名带回提交接口。
5. cURL 示例
将 API Key、API Secret 和已授权接收人的 OpenID 放入环境变量后执行。请将 requestId、客户信息和详情链接替换为实际业务值。
export CRM_PUSH_API_KEY='YOUR_CRM_API_KEY'
export CRM_PUSH_API_SECRET='YOUR_CRM_API_SECRET'
export CRM_RECEIVER_OPENID='YOUR_OPENID'
curl --fail-with-body 'https://openid.xianboss.com/api/v1/crm/push' \
-H "X-API-Key: $CRM_PUSH_API_KEY" \
-H "X-API-Secret: $CRM_PUSH_API_SECRET" \
-H 'Content-Type: application/json' \
--data-binary @- <<JSON
{
"requestId": "crm-customer-C0001-followup-20261026-001",
"openId": "$CRM_RECEIVER_OPENID",
"customerName": "测试客户",
"customerType": "潜在客户",
"customerSource": "CRM录入",
"customerPhone": "13800000000",
"markedAt": "2026-10-26 11:00",
"url": "https://crm.example.com/customers/C0001"
}
JSON
使用返回的消息 ID 查询:
curl --fail-with-body \
'https://openid.xianboss.com/api/v1/crm/messages/11111111-2222-4333-8444-555555555555' \
-H "X-API-Key: $CRM_PUSH_API_KEY" \
-H "X-API-Secret: $CRM_PUSH_API_SECRET"
6. Java 17 HttpClient 示例
以下类仅使用 Java 17 标准库。环境变量 CRM_PUSH_API_KEY、CRM_PUSH_API_SECRET、CRM_RECEIVER_OPENID 和 CRM_PUSH_REQUEST_ID 必须设置;同一业务通知的再次执行应继续使用相同 requestId 与相同内容。
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Set;
import java.util.regex.Pattern;
public class CrmPushExample {
private static final String BASE = "https://openid.xianboss.com";
private static final HttpClient HTTP = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.followRedirects(HttpClient.Redirect.NEVER).build();
public static void main(String[] args) throws Exception {
String key = env("CRM_PUSH_API_KEY");
String secret = env("CRM_PUSH_API_SECRET");
String openId = env("CRM_RECEIVER_OPENID");
String requestId = env("CRM_PUSH_REQUEST_ID");
String body = "{" +
"\"requestId\":" + quote(requestId) + "," +
"\"openId\":" + quote(openId) + "," +
"\"customerName\":\"测试客户\"," +
"\"customerType\":\"潜在客户\"," +
"\"customerSource\":\"CRM录入\"," +
"\"customerPhone\":\"13800000000\"," +
"\"markedAt\":\"2026-10-26 11:00\"," +
"\"url\":\"https://crm.example.com/customers/C0001\"}";
HttpRequest request = HttpRequest.newBuilder(URI.create(BASE + "/api/v1/crm/push"))
.timeout(Duration.ofSeconds(20)).header("X-API-Key", key).header("X-API-Secret", secret)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)).build();
HttpResponse<String> receipt = HTTP.send(request,
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
if (receipt.statusCode() != 200 && receipt.statusCode() != 202) {
throw new IllegalStateException("提交失败 HTTP " + receipt.statusCode() + ": " + receipt.body());
}
System.out.println("提交结果:" + receipt.body());
var idMatch = Pattern.compile("\\\"id\\\"\\s*:\\s*\\\"([0-9a-fA-F-]{36})\\\"")
.matcher(receipt.body());
if (!idMatch.find()) throw new IllegalStateException("响应缺少消息 ID");
String id = idMatch.group(1);
var statusPattern = Pattern.compile("\\\"status\\\"\\s*:\\s*\\\"([A-Z_]+)\\\"");
for (int attempt = 0; attempt < 15; attempt++) {
HttpRequest query = HttpRequest.newBuilder(URI.create(BASE + "/api/v1/crm/messages/" + id))
.timeout(Duration.ofSeconds(20)).header("X-API-Key", key).header("X-API-Secret", secret).GET().build();
HttpResponse<String> result = HTTP.send(query,
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
if (result.statusCode() != 200) {
throw new IllegalStateException("查询失败 HTTP " + result.statusCode() + ": " + result.body());
}
var state = statusPattern.matcher(result.body());
if (!state.find()) throw new IllegalStateException("响应缺少状态");
if (Set.of("SENT", "FAILED", "UNKNOWN").contains(state.group(1))) {
System.out.println("处理结果:" + result.body());
return;
}
Thread.sleep(2000);
}
System.out.println("任务仍在队列,请保存消息 ID 后稍后查询:" + id);
}
private static String env(String name) {
String value = System.getenv(name);
if (value == null || value.isBlank()) throw new IllegalArgumentException("缺少环境变量 " + name);
return value;
}
private static String quote(String value) {
StringBuilder out = new StringBuilder("\"");
for (char c : value.toCharArray()) {
if (c == '"' || c == '\\') out.append('\\').append(c);
else if (c < 0x20) out.append(String.format("\\u%04x", (int) c));
else out.append(c);
}
return out.append('"').toString();
}
}
保存为 CrmPushExample.java 后,可用 javac -encoding UTF-8 CrmPushExample.java 编译,再执行 java CrmPushExample。示例不会在网络异常后创建新的请求标识;业务系统可在持久化原 requestId 和请求内容后按前述幂等规则重试。
7. 错误与限制
应用返回的错误通常为:
{"code":"IDEMPOTENCY_CONFLICT","message":"同一 requestId 已用于不同消息,请为新消息生成新 requestId"}
| HTTP 状态 | 含义与处理 |
|---|---|
| 400 | 请求格式、字段、时间、URL 或额外字段不正确;也可能是 CHANNEL_DISABLED,表示 CRM 通道已停用 |
| 401 | Key 或 Secret 缺失、不匹配、尚未配置,或调用方已停用 |
| 403 | CHANNEL_FORBIDDEN:无通道权限;RECIPIENT_FORBIDDEN:该 OpenID 未授权给当前调用系统、已停用或已删除 |
| 404 | 消息不存在、属于其他调用方,或资源不存在 |
| 409 | IDEMPOTENCY_CONFLICT:同一 requestId 被用于不同内容 |
| 413 | 请求体超过 64KB |
| 429 | 接口限流,请延迟后使用原 requestId 重试 |
| 500 | 服务内部错误,请保留原请求标识并稍后查询或重试 |
| 503 | 数据库暂不可用,或 CRM_NOT_CONFIGURED 表示 CRM 通道/模板尚未配置,需要检查服务状态 |
常见 HTTP 400 业务错误:VALIDATION_ERROR(必填、长度或格式不符)、INVALID_CRM_REQUEST(请求非 JSON 对象、字段类型错误或含控制字符)、INVALID_CRM_TIME(无效日期时间)、INVALID_CRM_PHONE(电话号码不含数字)、UNSUPPORTED_CRM_FIELD(传入未支持字段)、INVALID_URL(详情链接格式错误)。
新客户跟进消息固定使用当前配置的“新客户跟进通知”模板。消息内容应对应实际 CRM 业务事件。微信最终拒绝的消息会进入 FAILED,原因见 lastError;例如模板 ID 无效、接收人未关注或微信平台限制。不要把所有错误都当作可自动重试的错误。
Nginx 入口按来源 IP 限流;请求正文最大 64KB。调用方应在提交前保存业务通知、requestId 和发送内容,再保存返回的消息 ID,便于状态追踪与人工处理。