news 2026/9/17 1:40:25

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
钉钉工作通知消息API开发实战:从access_token到消息撤回全指南

最近因为要给运维告警加一套消息推送,我把钉钉服务端API里工作通知消息这套接口完整过了一遍。这套接口的应用价值其实很大:企业内部系统需要把告警、审批、待办这类关键信息实时推给指定员工,工作通知消息是官方提供的标准通道,相比群机器人只能发群、短信还要花钱,它天然具备单点触达、已读回执、支持跳转深链的能力。适合看这篇内容的人很明确:企业内部应用开发者、运维工程师、信息化负责人,以及正在做钉钉生态集成的朋友。

我把整个开发链路拆开来说,从创建企业内部应用、申请权限,到获取access_token、发送各类消息,再到消息撤回和常见错误排查,全程都有可复现的步骤和代码,跟着走基本不会卡壳。

1. 从需求说起:为什么要在服务端调用钉钉API

1.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_type=client_credentials。正常返回会包含access_tokenexpires_in,expires_in固定是7200秒,也就是2小时。

用一个简单的Python请求就能拿到token:

import 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, params=params, timeout=5) 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(host="127.0.0.1", port=6379, decode_responses=True) 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, params=params, timeout=5).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_v2,POST请求,需要传两个固定参数:access_token和一系列业务参数。注意这个接口不是普通的JSON接口,它要求Content-Typeapplication/json,但参数是放在请求体里的业务字段。

参数里最关键的有四个:agent_id必填,是应用AgentId;userid_listdept_id_list至少选一个,接收人列表用逗号分隔,最多支持1000个UserID;msg是消息体,是一个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, params=params, headers=headers, data=json.dumps(payload), timeout=5) return resp.json()

请求成功后会返回一个task_id,这个task_id后面可以用来查询发送进度和撤回消息。errcode为0才算成功,其他情况对照错误码排查。

4.2 文本消息与Markdown消息的取舍

文本消息用法最简单,适合纯告警文本、通知公告,内容直接放在text.content字段里。但文本消息没有排版,长文本阅读体验较差,所以更多场景会选Markdown消息。Markdown消息的消息结构稍微复杂一点,msgtypemarkdown,需要传titletext两个字段,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里可以放titlecontentform表单列表,form列表每一项是keyvalue键值对;message_url设置整条消息的跳转链接。

ActionCard消息则更适合做带操作按钮的通知,消息会渲染成一张卡片,卡片底部可以有1到2个操作按钮,比如“查看详情”和“忽略”。在故障告警和重要审批场景里,这种交互非常有用,用户不需要离开钉钉就能完成“确认”或“跳转”的操作。

4.4 如何正确使用@能力与消息跳转链接

在工作通知消息里,@指定人和群聊里的@不太一样。文本消息的text.content里直接写@手机号是不会生效的,需要在消息体的at字段中传入atMobilesatUserIds,然后文本内容里仍然要包含对应的“@某个人”字样才会在渲染时高亮。

示例:

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_idtask_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}, headers=headers, data=json.dumps(payload), timeout=5) return resp.json()

5.2 已读回执与事件订阅

钉钉本身支持获取工作通知消息的已读状态,但不是直接“查已读”,而是通过事件订阅来异步推送。在开发者后台配置好事件订阅之后,钉钉会在用户读取消息时把ChatReadEvent事件推送到你的回调服务。回调请求体里包含taskIdcorpIduserIdList等字段,拿到这些字段就可以在业务系统里标记“这条消息张三已经读过了”。

回调URL需要处理加解密,官方提供了加解密库,语言有Java、Python、Go等几种,逻辑上对POST过来的加密字符串做AES解密,然后解析JSON事件。很多人在这一环被难住,我建议先在本地用官方示例代码跑通加解密流程,再接入业务逻辑,千万不要直接在生产环境裸调,回调报文里的时间戳和随机数校验是非常容易踩坑的地方。

5.3 发送频率限制与配额管理

工作通知消息整体上有频率限制。单应用发送消息的频率在较高并发下会被限流,触发限流之后接口会返回errcode 9001890002,提示“发送消息频繁”或者“请求过多”。不同版本的企业版,消息配额可能不同,基础免费版虽然有发送能力,但频率明显比专业版低。

我在做全员通知的时候就撞到过这个限制:一次性给800人发同一条Markdown,直接在中间被限流。后来改成按部门分批发送,每批200人,间隔2到3秒,基本就稳定了。建议所有批量发送都做分批重试,不要指望一次性打满。

6. 常见问题与排查技巧实录

6.1 高频错误码速查

errcode说明处理建议
0成功正常返回
88鉴权失败检查AppSecret,检查IP白名单
40078不存在的临时授权码检查AppKey和AppSecret是否匹配
40014不合法的access_token重新获取token,检查缓存层
60011无权限检查权限点是否申请并通过
60020不在访问白名单在开发者后台配置服务器出口IP
90002请求过于频繁降低频率,分批发送
90018发送消息频繁增加间隔,分片处理

这个表是我整理过的常用错误码,排查时对照这个表能省很多时间。如果错误码是7100171002这种,多半是UserID不是企业内部用户,接收人不在组织内,这种怎么重试都没用,只能让人事确保用户已经入群或加入组织。

6.2 容易被忽视的细节坑

UserID和部门ID最容易搞混。钉钉的UserID是字母和数字组成的字符串,部门ID是纯数字,二者完全不是一个体系。发送工作通知时userid_listdept_id_list可以同时传,但如果你把部门ID误传到userid_list里,返回的错误会很隐蔽,不是直接提示“部门ID不能传这里”,而是提示“没有找到用户”。

钉钉群发不了文件这个热词,让我想起工作通知消息里确实没有直接发送文件附件的接口。如果你想在通知里附带文件,常规做法是先把文件传到钉盘,再用链接消息把文件链接发出去。这也涉及到钉盘容量的问题,工作通知消息本身不占钉盘空间,但如果你的应用想存大量附件,要留意组织钉盘剩余空间,否则会碰到“显示钉盘容量不足”的情况,那就要清理历史文件或者扩容。

还有一个容易被忽略的坑是消息体的长度限制。Markdown消息的text字段官方建议不要超过2万字符,实际上我测下来超过1万字符时,部分手机端渲染会有卡顿或者内容被截断。长消息建议拆成多段发送,或者给一个详情链接。

6.3 HTTP状态码与返回结果不一致的排查方法

有时候HTTP状态码是200,但返回的errcode不是0,这时候很多新手会直接看HTTP状态码,以为成功了。实际上钉钉服务端API统一返回HTTP 200,真正的业务状态在errcodeerrmsg里。拿到非0的errcode不要慌,把errmsg记录下来,去文档里查对应错误码的含义。

调试阶段我建议把每个接口的请求参数和响应原文都打日志,入参打码后记录。比如发送工作通知时,把userid_listagent_idmsg整体打印出来,对比出现问题的消息和正常消息的差异,往往几秒钟就能定位问题。

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,一层一层往上叠,这样问题会很容易定位。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/17 1:40:01

职场可控混乱策略:低效背后的安全智慧

1. 效率悖论:为什么低效反而更安全?在职场中,我们常常被教导要追求高效、优化流程、提升产出。但有趣的是,在某些特定场景下,刻意保持一定程度的低效反而能带来意想不到的职业安全感。这种现象被称为"可控混乱&qu…

作者头像 李华
网站建设 2026/9/17 1:39:48

HiL测试工程师入门:CANoe环境构建与CAPL工程化实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 1:39:27

Ubuntu上用VirtualBox搭建Windows虚拟机:从安装到性能调优完全指南

1. 先判断场景:为什么要在Ubuntu里装Windows虚拟机1.1 我遇到过的几个典型需求先说个最实际的背景。我长期用Ubuntu做主力系统,日常写代码、跑容器、做自动化测试都没问题,但真正让我下决心装Windows虚拟机的,是几次被业务方打回来…

作者头像 李华
网站建设 2026/9/17 1:37:15

Ubuntu 24.04+RTX 5090部署OpenVLA-OFT及LIBERO评估实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华