PushMsgCRM 微信推送接口 · v1

CRM 微信推送接口调用文档

更新日期:2026-10-26。接口版本:v1。

CRM 提交客户信息后,服务将其映射为“仙宝科技”公众号的“新客户跟进通知”,持久化入队,再异步发送。CRM 无需管理公众号 AppSecret、微信 access_token、模板 ID 或微信字段名。

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”。请求标识区分大小写。

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,便于状态追踪与人工处理。