青鸾QINGLUAN
控制台

企业微信 API 接入指南

通过 api.qingluanbot.com 接入企业微信:创建 API Key、登录实例、发送消息与媒体,并接收 Webhook 事件。

青鸾企业微信能力开放平台。通过稳定的 HTTP API 调用企业微信能力:实例托管、消息收发、联系人 / 群聊 / 标签管理、朋友圈、文件与回调。

对接须知

接入流程

  1. 注册并登录开发者控制台 https://open.qingluanbot.com
  2. 「实例与回调」一键扫码登录企业微信,获得实例标识 appid
  3. 「API Key」创建一个 Key(请求时通过请求头携带)。
  4. API Key + appid 调用业务接口。
  5. 如需接收消息 / 事件,配置 Webhook 回调地址(见下方「Webhook 回调服务」)。

基础约定

  • 统一 API 地址https://api.qingluanbot.com
  • 请求POST /qingluan/api/{模块}/{动作}Content-Type: application/json
  • 鉴权:请求头 QL-Key: <你的 API Key>(也支持 Authorization: Bearer <key>
  • 实例:请求体携带 appid所有接口必填),它决定这次调用发给哪个企业微信号。 不传返回 400。请求体统一使用 appid,不要同时传入多个实例标识字段。
  • 响应原样返回,不额外包装信封。

⚠️ code = 0 的确切含义

code = 0 表示请求被接受,不保证副作用一定发生。个别接口在参数缺失或有误时 仍会返回 code = 0(例如发送类接口 content 结构不对时,会在会话里留下一条空消息)。

建议:发送前自行校验必填字段;对结果敏感的操作,用对应的查询接口复核一次 (发消息用「拉取消息」,群操作用「获取群资料」,朋友圈用「查询朋友圈详情」)。

媒体文件:先上传,再发送

图片 / GIF / 语音 / 文件不能直接传 URL 或二进制,要分两步 —— 上传接口返回的 data,整体作为发送接口的 content,字段逐个对应,不需要自己拼:

要发什么第一步第二步
图片api/cdn/uploadImageapi/message/sendImagecontent = 上一步的 data
GIFapi/cdn/uploadImageapi/message/sendGifcontent = 上一步的 data
文件api/cdn/uploadFileapi/message/sendFilecontent = 上一步的 data
语音api/cdn/uploadFileapi/message/sendVoicecontent = 上一步的 data voiceTime

⚠️ 大整数精度(JS 客户端必读)

响应里的 roomId / conversationId / uin 是 17~19 位整数,超过 JavaScript 的 Number.MAX_SAFE_INTEGER(9007199254740991)。浏览器 JSON.parse静默取近似值 (例:10952820944829695 变成 10952820944829696),末位出错且不抛异常。

JS / TypeScript 客户端必须用 JSON.parse 的 reviver、或按文本正则取值,不要直接 parse 后当数字用。 这是「原样返回」的固有代价:转成字符串就不叫原样了。

调用频率限制

每个开发者账号 每分钟 300 次(按账号计,不按 Key —— 开多把 Key 不会增加额度)。

超限返回 HTTP 429,业务码 -42900,并带标准的 Retry-After 头(单位:秒):

json
{ "code": -42900, "message": "调用过于频繁,请 N 秒后重试。…", "data": null }

正确的处理方式是Retry-After 并退避,不要立刻重试 —— 立刻重试只会继续消耗额度。

这个额度是按实际用量定的:现网每分钟调用量 p99 是 7 次、历史峰值 26 次, 300 已是十倍以上余量,正常业务不会碰到。触发它通常意味着你的客户端在重试循环里。 如果你的业务确实需要更高频率(批量导入、历史数据回填等),联系客服单独调整, 不必自己想办法绕。

这几个接口有额外行为

参数和返回都与其余接口一致,直接调即可;差别在于青鸾会在中间多做一步, 免得你自己再搭一遍:

接口青鸾额外做了什么
api/device/create先校验你的账号额度与有效期,建成后把 appid 登记到你名下 —— 不登记的话它过不了后续接口的归属校验,等于建了个用不了的设备
api/long/startapi/long/updateCallbackURL你传的 callbackUrl 会被记为你的接收地址;报给能力层的是青鸾的接收器。事件先到青鸾,再带青鸾签名转发给你,并附带失败重投与去重。你收到的包体结构见「回调(事件推送)」
api/long/stop关闭后青鸾的定时探活不会再自动把它拉起来;下次调 api/long/start 即恢复自动维护

callbackUrl 必须是可公网访问的 http(s) 地址,且不能填青鸾自己的接收地址。

错误码

青鸾自己产生的错误一律用负数 code,不会和能力层的业务码混淆(能力层的码见各接口说明)。 HTTP 状态与 code 一一对应,判断其一即可。

codeHTTP含义怎么处理
401401API Key 缺失、无效或已撤销检查请求头 QL-Key
-40001400缺少 appid,或请求体不是 JSON 对象补上 appid;确认 Content-Type: application/json
-40002400实例标识字段名不对,或同时传了多种写法且值不一致只传一个 appid
-40004400请求路径含义不明确(多余斜杠、点段等)用规范路径 /qingluan/api/{模块}/{动作}
-40010400配置回调时缺少 appid补上 appid
-40011400callbackUrl 不是 http(s) 地址换成 http(s) 开头的完整地址
-40012400回调地址填成了青鸾的接收地址填你自己服务的公网地址
-40013400回调地址不可用:域名解析不到,或落在内网 / 环回地址换一个可公网访问的地址
-40402402/403账号额度已满或权益过期(建设备时)到控制台查看剩余额度与有效期,或联系客服
-40403403在不支持的接口上传了 callbackUrl / pushHistory回调地址只在 api/long/startapi/long/updateCallbackURLPOST /qingluan/instances/set-callback 上有效
-40404404appid 不存在或不属于你核对控制台「实例与回调」里的值
-42900429调用过于频繁按响应头 Retry-After 退避;确需更高频率请联系客服
-50000502回调登记失败稍后重试
-50002502服务暂时不可用(超时 / 网络异常 / 未就绪)退避重试;持续失败请联系客服

⚠️ -40404「不存在」「不属于你」是同一句话,这是有意的:区分开等于给攻击者一个 枚举探针,可以拿别人的 appid 试出哪些是真实存在的。

Webhook 回调服务

事件走中转,不是直连:

text
企业微信 ──▶ 青鸾接收器 ──┬──▶ 回调日志(控制台,原样留存 24 小时)
                              └──▶ 转发到你的地址(青鸾信封 + HMAC 签名)

企业微信事件只推给青鸾接收器 —— 这个地址一号一个、扫码上号时自动注册、不可更改也不可关闭。 它是回调日志、归属校验、掉线自动重连三件事的共同前提。

⚠️ 回调只有入站。 你自己通过接口发出去的消息不会产生回调 —— 这是正常的, 不是回调丢了。要确认自己发的消息,用 api/message/sync 拉取。

配置转发地址(三选一,优先级从高到低):

方式作用范围
控制台「实例与回调」→ 某个实例「配置回调」只这个号
控制台「实例与回调」→ 账号默认转发地址所有没单独配的号
POST /qingluan/instances/set-callback(body: appid + callbackUrl只这个号

请求鉴权(可选):在控制台配置回调地址时,可以开启 Bearer 鉴权并填写由你自己 管理的 Token。开启后,青鸾的每次回调 POST 都会携带:

http
Authorization: Bearer <你配置的 Token>

Token 会加密保存,保存后不再回显;留空表示保持原 Token,关闭鉴权或清空对应回调地址 会同时清除它。实例没有独立回调地址时,会连同地址和签名密钥一起继承账号默认配置。 Bearer 鉴权与下方 HMAC 验签彼此独立,开启 Bearer 后仍会保留全部 HMAC 请求头。

保存时的测试回调:在控制台保存非空回调地址时,青鸾会立即向该地址直发一次 QingluanCallbackTest,正文事件内容固定为“青鸾回调测试”。测试请求沿用正式回调的 HMAC 请求头;如果该地址开启了 Bearer 鉴权,也会携带相同的 Authorization 请求头。

实例回调测试会携带该实例的真实 appid

json
{
  "appid": "<实例 appid>",
  "event_type": "QingluanCallbackTest",
  "test": true,
  "events": [
    {
      "id": "cbtest_<唯一值>",
      "content": "青鸾回调测试",
      "created_at": "<UTC ISO 8601 时间>"
    }
  ]
}

账号默认回调不对应某一个实例,因此测试正文不含任何实例标识:

json
{
  "event_type": "QingluanCallbackTest",
  "test": true,
  "events": [
    {
      "id": "cbtest_<唯一值>",
      "content": "青鸾回调测试",
      "created_at": "<UTC ISO 8601 时间>"
    }
  ]
}

这是一条一次性连通性测试:不进入正式投递队列、不自动重试,也不出现在回调预览里。 配置会先保存;即使测试超时、连接失败或目标返回非 2xx,也不会回滚。界面显示测试成功 只表示目标返回了 2xx,不代表目标一定已经完成验签或业务处理。

转发信封

json
{
  "appid": "<与业务接口用的是同一个值>",
  "event_type": "Msg",
  "events": [ ... ]
}

event_type 是事件类型(见下方「事件类型」)。一个地址收多个号时,按 appid 区分。

验签(可选,不验也能正常收):请求头 X-Qingluan-Timestamp(Unix 秒)与 X-Qingluan-Signature

text
signature = HMAC-SHA256(key   = callback_secret 的 UTF-8 字节,
                        msg   = timestamp 的 ASCII 字节 + b"." + 原始请求体字节)
            的十六进制小写

三个容易签错的地方:

  1. timestamp 和一个点号也在被签的消息里,不是只签请求体。
  2. 收到的原始字节,不要先反序列化再重新序列化 —— 键顺序和空格一变签名就对不上。
  3. callback_secret 直接当 UTF-8 字节做密钥,不做 base64 / hex 解码。

固定测试向量(拿它先把本地实现校准了,再去接真实回调):

callback_secretwhsec_demo
X-Qingluan-Timestamp1757000000
原始请求体{"appid":"we_demo","event_type":"Msg","events":[]}
期望签名cc555c9b81e1c9652c00df7d53e2e6711996d614df665dbe3dab0454fa48f41b

时间窗:我方不限制 X-Qingluan-Timestamp 的新旧(重试最长可能在 6 小时后到达, 卡窗会把正常重投判成伪造)。建议你按自己的容忍度校验,取 ±10 分钟是常见选择; 重投场景下请以事件去重为主、时间窗为辅。

密钥在控制台可见,账号级与实例级各有一个,与地址成对使用。轮换密钥后, 尚未送达的重试会用新密钥签名 —— 旧密钥不再保留。

投递保证:至少一次(at-least-once)

  • 投递失败(超时、连接失败、非 2xx)会按 30 秒 / 2 分 / 10 分 / 30 分 / 2 小时 / 6 小时 退避重投,共 6 次;只有 2xx 才算送达(3xx 不算 —— 我们不跟随跳转)。
  • 6 次都失败进死信,可在控制台「回调投递」里查看并手动重放。
  • 因此同一条事件你可能收到多次(重投、或平台自身重推)。请按 events[].id 做幂等;没有 id 的事件(如 GapClosed)按你自己的业务键去重。
  • 先落库再回 200。你回了 2xx 我们就认为送到了,不会再投第二次。

事件类型

event_type(一级事件,标明这一包是什么):

event_type含义
Msg消息 / 群事件,具体看 events[].contentType
GapConnected / GapSucceed长连接已建立 / 就绪
GapClosed长连接断开,code 见下表
BigFileUploadCompleted / BigFileUploadFailed大文件上传结果
Error错误
InstanceOffline / InstanceRecovered青鸾自己发的账号状态变化,见下方「账号状态事件」

GapClosedcode

code含义账号是否还在线
-11001需断线重连后判断可能仍在线,青鸾会自动重连确认
-11002已在其他设备上登录
-1008移动端主动退出登录
-1长连接异常,见 message
0用户手动关闭长连接

账号状态事件(青鸾自产)

上面那些是能力层推给你的。InstanceOffline / InstanceRecovered 不一样, 是青鸾在探测到账号状态变化时自己发的 —— 因为能力层会静默断: 连接掉了却不发 GapClosed,只表现为消息突然不来了。青鸾每分钟探一次, 断了先自动尝试免扫码重连(沿用登录时的同一出口),救不回来才通知你。

信封与其他事件完全一致,你已有的解析和按 events[].id 的幂等逻辑不用改:

json
{
  "appid": "we_xxxxxxxxxxxxxxx",
  "event_type": "InstanceOffline",
  "events": [{
    "id": "InstanceOffline:we_xxxxxxxxxxxxxxx:1757500000",
    "reason": "长连接中断,自动重连未成功",
    "action_required": "relogin",
    "detected_at": "2026-09-10T13:31:00+00:00",
    "last_seen_at": "2026-09-10T12:56:36+00:00"
  }]
}

action_required 决定你该怎么处理:

含义该做什么
relogin长连接断了,登录态多半还在到控制台点「重新登录」,通常免扫码
manual风控 / 已在其他设备登录 / 手机端主动退出必须重新扫码。这类反复重连会加重风控,青鸾已停止自动重试

InstanceRecoveredevents[]recovered_atoffline_seconds (本次中断时长)和 auto_reconnected(是否由青鸾自动救回)。

⚠️ 只在状态翻转时各推一条,不会每轮重复推。短暂抖动被当轮重连救回的, 不会打扰你。

events[].messageType(会话维度):0=私聊 · 1=群聊 · 3=应用/系统会话。 ⚠️ 不要用 roomId != 0 判群聊 —— messageType=3roomIdfromUserId 同值, 用它判会把应用会话误判成群聊。一律以 messageType 为准。

events[].contentType(消息类型):

文本0 2 文本(content[{type,text}] 数组)
媒体14 101 图片 · 16 语音 · 23 103 视频 · 15 102 文件 · 20 大文件 · 29 104 表情
富消息13 链接 · 6 位置 · 41 名片 · 78 小程序 · 4 聊天记录 · 26 红包 · 215 笔记 · 579 会议卡片 · 10 邮件通知
群事件1001 修改群名 · 1002 成员进群 · 1003 移除群成员 · 1005 退群 · 1006 群新增 · 1011 群操作提示 · 1022 群设置/群主变更 · 1023 群解散 · 1029 进群邀请申请 · 1043 群管理员变动 · 2118 群信息变动 · 213 群接龙 · 2308 群公告变更 · 2063 撤回
通话2324 通话通知 · 40 通话结果 · 2350 通话结束 · 2412 通话记录提示 · 503 2120 2166 通话信令
其它2001 已读回执 · 2357 好友申请
纯信令2002 2055 2104 2114 2115 2130 2131 2132 2160 2161 2180 2186 2188 2201 2215 2313 —— content 为空,无可读内容,建议直接过滤

三条要点,照做能少走弯路:

  1. 进出群事件靠载荷形态区分1002 成员进群与 1003 移除群成员同构 (fromUserId=操作者,content=被操作成员的 uid);1005 退群则 fromUserId=退群者本人content为空。 ⚠️ 1006群新增、不是退群,别拿它判退群。 ⚠️ 2118 群信息变动的 content 是空的,但它不是无意义的信令 —— 例如微信成员自己退群就会推它。别按"载荷为空"一律过滤掉。
  1. 群系统事件的正文是 protobuf,句子的主语是一个数字 id,不是文字。 例:1022 群主变更解出来是 {1: <userId>, 2: "已经成为新的群主"} —— 光读文字看不出是谁。只有 id,没有名称,要显示成人名得自己拿 id 去查成员资料。
  2. 群公告的正文和事件是分开的两条。 公告正文是一条 contentType 2(文本), 靠 flag0x10000 位与普通消息区分;contentType 2308(群公告变更) 只带那条公告消息的 id、不带正文 —— 想显示内容得拿这个 id 回查那条消息。
  3. 一次音视频通话会推 8~10 条事件,不是一条。发起固定 3 条;结束按结局不同, contentType 40 的正文就是结局本身:
结局40 的正文是否有 2412总条数
主叫取消对方已取消8
被叫拒接已拒绝9
超时未接未接听9
接通后挂断通话时长10

2350 每种结局都会推,代表通话结束;2412 只在接通后出现(录音与 AI 总结提示)。

来电渠道不同,信令类型也不同:企业微信内部通话是 2120 + 503微信用户打来2166 + 503,且不推 23502412503 是两种渠道共有的那条,2120/2166 按渠道区分。 ⚠️ 两者成对出现、内容相同 —— 请按通话会话号去重、只处理其一, 否则每通电话会重复计一次。

  1. 资料变更只推信令、不推内容。 改备注 / 标签 / 描述 / 聊天标签,会收到 2313 2160 2161 2186 2131 等一串信令,但它们的 content空的 —— 企业微信只告诉你「有东西变了」,不告诉你变成什么。要拿新值必须收到信令后 主动调接口查,别指望从回调里解析。
  1. 2357 好友申请的 content 是结构化字段(不是 hex): corpId / uin / corpName / customerName / source。 其中 source 直接写明来源(例如 微信),是判断对方是不是微信用户最可靠的字段。

接口清单

左侧列出的是当前能力层已开放的接口。由于数据面是原样透传,清单可能滞后于能力层 —— 新增接口即使还没出现在文档里,也可以直接按 POST /qingluan/api/{模块}/{动作} 调用。

搜索接口

输入关键词查找接口。最多显示 20 项。