钉钉工作通知消息API开发实战:从access_token到消息撤回全指南

发布时间:2026/9/17 1:40:26
钉钉工作通知消息API开发实战:从access_token到消息撤回全指南 最近因为要给运维告警加一套消息推送我把钉钉服务端API里工作通知消息这套接口完整过了一遍。这套接口的应用价值其实很大企业内部系统需要把告警、审批、待办这类关键信息实时推给指定员工工作通知消息是官方提供的标准通道相比群机器人只能发群、短信还要花钱它天然具备单点触达、已读回执、支持跳转深链的能力。适合看这篇内容的人很明确企业内部应用开发者、运维工程师、信息化负责人以及正在做钉钉生态集成的朋友。我把整个开发链路拆开来说从创建企业内部应用、申请权限到获取access_token、发送各类消息再到消息撤回和常见错误排查全程都有可复现的步骤和代码跟着走基本不会卡壳。1. 从需求说起为什么要在服务端调用钉钉API1.1 工作通知消息和群机器人消息的区别很多人一开始会把群机器人消息和工作通知消息混为一谈实际上这是两套完全不同的能力。群机器人消息的入口是我们常见的自定义机器人Webhook适合往一个群里面推消息比如zabbix告警直接推到运维群但它的局限很明显无法定向发给某个人也没办法知道谁读了谁没读安全性上只要拿到Webhook地址就能往里发。工作通知消息走的是服务端API由企业内部应用发起消息会出现在员工的“工作通知”会话里体验上像公众号发消息一样。它有四个核心优势一是明确指定接收人按UserID或者部门ID下发二是消息类型丰富支持文本、Markdown、OA审批、ActionCard、链接卡片等格式三是能拿到发送结果和已读状态四是天然支持跳转企业内部应用页面。对于“必须送达、必须被看到”的场景比如故障告警、审批待办、工单通知工作通知消息才是正解。1.2 典型应用场景盘点从我实际接触过的项目来看这套API覆盖的场景非常集中。运维方向最常见的是监控平台联动比如zabbix 7.0、Prometheus、Grafana告警推送到责任人工作通知中文社区里很多人在做zabbix 7.0联动钉钉除了群机器人就是走工作通知。研发管理方向禅道搭建起来之后任务指派、Bug分配、需求评审结果都可以通过工作通知推到个人配合钉钉自带的审批能力体验很顺。内部信息化场景同样不少。比如考勤异常提醒很多公司用钉钉的定位打卡、扫脸打卡考勤系统检测到异常后可以通过工作通知当天推送培训平台像钉钉酷学院学员报名成功、课程即将开始、考试结果公布这些通知走工作通知比短信便宜也更精准人力资源的场景入职流程、转正提醒、合同到期预警也都能串起来。适合来做这套东西的人画像也清晰手里有内部系统需要与钉钉打通想用最小成本实现消息触达闭环。2. 准备工作在企业内部应用里把地基打牢2.1 用一篇文章讲清企业内部应用的创建流程要调用服务端API第一步不是写代码而是先有一个合法的应用身份。登录钉钉开发者后台入口在工作台页面的右上角“开发者后台”进入后选择“企业内部应用”点击创建应用。这里要注意创建应用的类型决定后续的权限模型企业内部应用只能被本组织使用权限审批相对简单第三方应用则是给外部企业用的SaaS应用需要上架审核周期长、要求多。大多数自用场景选企业内部应用就行。创建时需要填写应用名称、描述、图标等基础信息提交后进入应用详情页这里有几个关键信息要盯紧AppKey、AppSecret、AgentId。AppKey相当于应用的账号AppSecret相当于密码AgentId是发送工作通知时必须用到的一个数字ID对应你这个应用在组织内的身份标识。光有这些还不够还要确认应用凭证状态是启用否则后续所有API都会返回“应用未启用”的错误。2.2 权限点申请与授权范围企业内部应用创建好之后默认没有权限调用工作通知接口。在应用详情页的“权限管理”里搜索“工作通知”或直接找“消息通知”分类会看到“获取待读消息”“发送工作通知”“撤回工作通知消息”等权限点。重点申请三个发送工作通知消息、撤回工作通知消息、获取工作通知消息的发送进度。除了消息权限通常还会用到通讯录相关的权限因为发送工作通知时需要传UserID如果只知道手机号就得先调用通讯录接口把手机号转成UserID。这里有一个天然的前提如果员工还没加入你的企业组织工作通知消息是发不出去的所以一定要先确认接收人已经是组织内成员。权限申请之后不是立刻生效的。企业内部应用的基础权限一般几分钟到几小时就能审核通过某些敏感权限比如读取全部通讯录可能需要管理员在管理后台手动审批。建议提前申请别等代码写完了才发现权限没下来。2.3 配置服务器出口IP与回调事件开发者后台还有几个配置和后续接口是否调用成功强相关。一是服务器出口IP白名单如果你设置了IP白名单钉钉服务端只接受来自白名单内IP的请求这里的IP是服务器公网出口IP不是开发机器内网IP。如果你部署在云服务器上直接填云服务器的公网IP即可如果公司网络是固定IP填办公网的出口IP如果是动态IP建议先别启用白名单或者用一台固定出口的跳板机发请求。二是事件订阅配置。如果你需要知道消息是否已读、用户点击了消息卡片、消息接收失败等动态就要在“事件与回调”里配置订阅。钉钉会以HTTP POST的方式把事件推送到你填写的回调URL上回调URL必须是公网可访问的HTTPS地址且需要完成加解密配置。这个放在后面回调部分再说但准备工作阶段就要把URL和加解密的密钥定下来。3. 从access_token说起这个通行证不简单3.1 获取access_token的调用方式所有服务端API请求都离不开access_token它是调用方的临时凭证。获取接口地址是https://oapi.dingtalk.com/gettoken请求方式GET需要带上三个参数appkey、appsecret还有一个固定的grant_typeclient_credentials。正常返回会包含access_token和expires_inexpires_in固定是7200秒也就是2小时。用一个简单的Python请求就能拿到tokenimport requests def get_access_token(app_key: str, app_secret: str) - str: url https://oapi.dingtalk.com/gettoken params { appkey: app_key, appsecret: app_secret, grant_type: client_credentials } resp requests.get(url, paramsparams, timeout5) data resp.json() if data.get(errcode) ! 0: raise Exception(f获取access_token失败: {data}) return data[access_token]这里有个点很多人没留意gettoken接口的返回有时不会等几秒钟就报错更多时候是因为请求中混入了非ASCII字符或特殊字符导致签名或参数解析失败。建议所有参数都做URL编码并且定期轮换AppSecret尤其是内部应用多、人员流动大的团队。3.2 缓存access_token的正确姿势access_token有效期只有7200秒而日常发消息的频率可能远超这个周期如果每次都重新获取一是白白多打一次接口二是有可能触发接口限流。正确做法是缓存起来全局只保留一个有效token快过期时再刷新。在Python项目里最简单的方案是用Redis存设置过期时间7000秒给一点冗余量import requests import redis r redis.Redis(host127.0.0.1, port6379, decode_responsesTrue) def get_cached_access_token(app_key: str, app_secret: str) - str: token r.get(dingtalk_access_token) if token: return token url https://oapi.dingtalk.com/gettoken params { appkey: app_key, appsecret: app_secret, grant_type: client_credentials } resp requests.get(url, paramsparams, timeout5).json() if resp.get(errcode) ! 0: raise Exception(f获取access_token失败: {resp}) r.setex(dingtalk_access_token, 7000, resp[access_token]) return resp[access_token]如果你用的是多实例部署Redis方案依然成立但要注意加锁避免多个实例同时发现token过期、同时去刷新。更简单一点可以直接在内存里放一个带过期时间的全局变量够用就行不一定要上Redis但如果你有多套环境共用同一个AppKey比如测试环境和生产环境共用那就必须用Redis或数据库存否则会被互相顶掉。3.3 高频Token报错排查我在实际项目里遇到过几类和token相关的典型问题。第一类是errcode 40078提示“不存在的临时授权码”这个一般是AppKey和AppSecret不匹配或者用了旧版本的密钥第二类是errcode 88提示“鉴权失败”通常是AppSecret填错了或者请求发的频率太高被临时拦截第三类是errcode 40014提示“不合法的access_token”最常见原因是token过期了但你的缓存层没有及时刷新。还有一个小众但容易被坑到的问题如果你同时接了钉钉的旧版API和新版API比如既用oapi.dingtalk.com又用api.dingtalk.com两边的token是不通用的。新版OpenAPI的token获取要走https://api.dingtalk.com/v1.0/oauth2/accessToken返回的也是独立的token。我在从旧接口迁移到新接口时就被这个坑了一把排查了半天才发现是token串用了。4. 发送工作通知消息的完整实现4.1 核心接口接口结构与参数解读发送工作通知消息的API地址是https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2POST请求需要传两个固定参数access_token和一系列业务参数。注意这个接口不是普通的JSON接口它要求Content-Type是application/json但参数是放在请求体里的业务字段。参数里最关键的有四个agent_id必填是应用AgentIduserid_list和dept_id_list至少选一个接收人列表用逗号分隔最多支持1000个UserIDmsg是消息体是一个JSON对象具体结构取决于消息类型还有一些可选参数比如to_all_user设为true可以全员发送这个慎用一旦误发就是全公司都知道。先看一个最基础的文本消息示例用Python的requests库实现import requests import json def send_work_notice(access_token: str, agent_id: int, user_ids: list, content: str): url https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2 headers {Content-Type: application/json} msg { msgtype: text, text: { content: content } } payload { agent_id: agent_id, userid_list: ,.join(user_ids), msg: msg } params {access_token: access_token} resp requests.post(url, paramsparams, headersheaders, datajson.dumps(payload), timeout5) return resp.json()请求成功后会返回一个task_id这个task_id后面可以用来查询发送进度和撤回消息。errcode为0才算成功其他情况对照错误码排查。4.2 文本消息与Markdown消息的取舍文本消息用法最简单适合纯告警文本、通知公告内容直接放在text.content字段里。但文本消息没有排版长文本阅读体验较差所以更多场景会选Markdown消息。Markdown消息的消息结构稍微复杂一点msgtype为markdown需要传title和text两个字段text里支持标准Markdown语法包括标题、加粗、链接、引用块。我这里有一个实际坑要提醒工作通知Markdown消息里的图片不能用普通外链钉钉会做转义处理常见的OSS外链绝大多数情况不显示只能显示纯文本和基础排版。如果你一定要在消息里带图要么用ActionCard消息里的图片字段要么给一个图片链接让用户点进去看。Markdown消息发送示例markdown_msg { msgtype: markdown, markdown: { title: 线上服务异常, text: ## 服务异常通知\n\n**应用名**: order-service\n**环境**: 生产\n**错误率**: 5.2%\n\n[点击查看详情](https://ops.example.com/alert/12345) } }这个我在实际告警推送中用的最多把核心指标用加粗和列表整理好责任人打开工作通知一眼就能看到关键信息不需要再点进系统。4.3 OA消息与ActionCard消息的场景差异OA消息是钉钉面向审批流设计的一种消息类型格式上支持头部、正文、表单、富文本还能设置消息跳转URL典型的OA消息长得很像一张结构化表单适合发送审批待办、工单信息、流程提醒这类场景。OA消息的结构比较复杂核心字段在msg.oa下面head里可以设置背景色和标题body里可以放title、content、form表单列表form列表每一项是key和value键值对message_url设置整条消息的跳转链接。ActionCard消息则更适合做带操作按钮的通知消息会渲染成一张卡片卡片底部可以有1到2个操作按钮比如“查看详情”和“忽略”。在故障告警和重要审批场景里这种交互非常有用用户不需要离开钉钉就能完成“确认”或“跳转”的操作。4.4 如何正确使用能力与消息跳转链接在工作通知消息里指定人和群聊里的不太一样。文本消息的text.content里直接写手机号是不会生效的需要在消息体的at字段中传入atMobiles或atUserIds然后文本内容里仍然要包含对应的“某个人”字样才会在渲染时高亮。示例text_msg_with_at { msgtype: text, text: { content: 您的工单已派发请及时处理。张三 }, at: { atUserIds: [zhangsan_userid] } }跳转链接同样有讲究。如果你想通过工作通知引导用户进入内部系统链接需要做URL编码并且如果链接是HTTPS且带参数服务端在解析时偶尔会丢参数建议所有动态参数都放在链接末尾并用encodeURIComponent处理。5. 消息发出去之后撤回、回执与配额5.1 撤回消息的时机与接口工作通知消息发出去后如果发现内容有误可以在24小时内撤回。撤回接口是https://oapi.dingtalk.com/topapi/message/corpconversation/recall参数是agent_id和task_id。task_id在你发送成功时的返回里会有所以发送成功之后一定要把task_id存下来否则后面想撤回都没办法。撤回有个限制要提前知道只能撤回发给企业内部员工的工作通知消息且消息必须在24小时内超过24小时就无法撤回了。另外如果消息已经被用户删除撤回接口依然会返回成功但用户那边的消息已经没了这属于正常现象。示例代码def recall_message(access_token: str, agent_id: int, task_id: int): url https://oapi.dingtalk.com/topapi/message/corpconversation/recall headers {Content-Type: application/json} payload { agent_id: agent_id, task_id: task_id } resp requests.post(url, params{access_token: access_token}, headersheaders, datajson.dumps(payload), timeout5) return resp.json()5.2 已读回执与事件订阅钉钉本身支持获取工作通知消息的已读状态但不是直接“查已读”而是通过事件订阅来异步推送。在开发者后台配置好事件订阅之后钉钉会在用户读取消息时把ChatReadEvent事件推送到你的回调服务。回调请求体里包含taskId、corpId、userIdList等字段拿到这些字段就可以在业务系统里标记“这条消息张三已经读过了”。回调URL需要处理加解密官方提供了加解密库语言有Java、Python、Go等几种逻辑上对POST过来的加密字符串做AES解密然后解析JSON事件。很多人在这一环被难住我建议先在本地用官方示例代码跑通加解密流程再接入业务逻辑千万不要直接在生产环境裸调回调报文里的时间戳和随机数校验是非常容易踩坑的地方。5.3 发送频率限制与配额管理工作通知消息整体上有频率限制。单应用发送消息的频率在较高并发下会被限流触发限流之后接口会返回errcode 90018或90002提示“发送消息频繁”或者“请求过多”。不同版本的企业版消息配额可能不同基础免费版虽然有发送能力但频率明显比专业版低。我在做全员通知的时候就撞到过这个限制一次性给800人发同一条Markdown直接在中间被限流。后来改成按部门分批发送每批200人间隔2到3秒基本就稳定了。建议所有批量发送都做分批重试不要指望一次性打满。6. 常见问题与排查技巧实录6.1 高频错误码速查errcode说明处理建议0成功正常返回88鉴权失败检查AppSecret检查IP白名单40078不存在的临时授权码检查AppKey和AppSecret是否匹配40014不合法的access_token重新获取token检查缓存层60011无权限检查权限点是否申请并通过60020不在访问白名单在开发者后台配置服务器出口IP90002请求过于频繁降低频率分批发送90018发送消息频繁增加间隔分片处理这个表是我整理过的常用错误码排查时对照这个表能省很多时间。如果错误码是71001、71002这种多半是UserID不是企业内部用户接收人不在组织内这种怎么重试都没用只能让人事确保用户已经入群或加入组织。6.2 容易被忽视的细节坑UserID和部门ID最容易搞混。钉钉的UserID是字母和数字组成的字符串部门ID是纯数字二者完全不是一个体系。发送工作通知时userid_list和dept_id_list可以同时传但如果你把部门ID误传到userid_list里返回的错误会很隐蔽不是直接提示“部门ID不能传这里”而是提示“没有找到用户”。钉钉群发不了文件这个热词让我想起工作通知消息里确实没有直接发送文件附件的接口。如果你想在通知里附带文件常规做法是先把文件传到钉盘再用链接消息把文件链接发出去。这也涉及到钉盘容量的问题工作通知消息本身不占钉盘空间但如果你的应用想存大量附件要留意组织钉盘剩余空间否则会碰到“显示钉盘容量不足”的情况那就要清理历史文件或者扩容。还有一个容易被忽略的坑是消息体的长度限制。Markdown消息的text字段官方建议不要超过2万字符实际上我测下来超过1万字符时部分手机端渲染会有卡顿或者内容被截断。长消息建议拆成多段发送或者给一个详情链接。6.3 HTTP状态码与返回结果不一致的排查方法有时候HTTP状态码是200但返回的errcode不是0这时候很多新手会直接看HTTP状态码以为成功了。实际上钉钉服务端API统一返回HTTP 200真正的业务状态在errcode和errmsg里。拿到非0的errcode不要慌把errmsg记录下来去文档里查对应错误码的含义。调试阶段我建议把每个接口的请求参数和响应原文都打日志入参打码后记录。比如发送工作通知时把userid_list、agent_id、msg整体打印出来对比出现问题的消息和正常消息的差异往往几秒钟就能定位问题。7. 扩展把工作通知真正用起来7.1 zabbix 7.0联动钉钉的实践思路zabbix 7.0联动钉钉算是中文社区里问得很多的需求。之前流行的方案是用zabbix的媒介类型配置一个Webhook调用钉钉自定义机器人把告警推到运维群。但这种方式面向的是群如果你想让具体某个负责人收到工作通知就要让zabbix调用你自己的消息服务接口由你的后端服务去调用钉钉服务端API。链路大概是这样的zabbix告警触发时在动作里调用一个HTTP请求把告警内容和负责人手机号传给内部的服务端服务端收到后用手机号查通讯录拿到UserID再调用工作通知接口发Markdown消息。这样做的好处是告警可以按级别、按系统、按负责人做精细路由而不是一股脑全推到群里。7.2 禅道搭建之后如何与钉钉打通禅道是做项目管理和Bug跟踪的系统很多团队搭建完禅道后最头疼的就是任务通知靠邮件邮件经常没人看。可以写一个消息桥接服务定时或实时读取禅道的API开放接口把新增指派给我的任务、状态变化的Bug、评审结果第一时间推送到对应人的钉钉工作通知。禅道默认没有完整的Webhook能力但它的API接口足够丰富拿到任务数据后组装成Markdown发送即可。这类桥接服务建议单独部署不要塞到禅道进程里避免互相影响。消息推送失败要有重试机制我在桥接层做了三档重试5秒、30秒、5分钟超过三次就转存数据库人工补发。7.3 钉钉机器人与小程序、考勤场景的组合玩法群里经常看到的钉钉机器人和工作通知消息是互补关系。机器人适合做群内广播比如发布公告、值班提醒工作通知适合做点对点的强触达比如审批未处理、待办超时。同一个事件可以两者一起用群里机器人发一条汇总工作通知给每个负责人发一条个人待办。在考勤场景企业普遍用定位打卡、扫脸打卡这些方式考勤机的状态变化、员工打卡异常提醒、外勤打卡审批通知全部可以走工作通知。这里要说明的是我只是讲技术接入的方式不鼓励任何绕过考勤规则的行为考勤系统本身也有防作弊机制做技术集成的还是要把正道做稳。钉钉里做小程序、对接钉钉酷学院这些场景同样可以利用工作通知做消息闭环小程序里的待办通知、培训课程开课提醒都通过服务端API下发让用户点击消息卡片直接跳回小程序页面。这个思路我个人非常推荐接入成本低用户体验提升明显实际使用起来会发现钉钉真的能串起一个组织内部的大部分消息流。我个人在实际开发中的体会是工作通知这套API最大的价值并不在“发一条消息”而在于让企业内部系统有了一个标准的、可靠的触达通道。踩过几次坑之后你会慢慢养成几个好习惯——token缓存一定要做task_id务必落库批量发送必须分批限流回调加解密先用官方样例跑通。我自己后来把一个经验固化成了一个小函数所有发送请求统一包装入参记录日志返回非0直接告警到运维群。如果你也要接这套接口建议从最小的文本消息跑通链路再加Markdown、OA、ActionCard一层一层往上叠这样问题会很容易定位。