回调通知
开票、红冲、作废等操作都是异步完成的。在 开具发票 或 红冲 时传入 callbackUrl,EasyAPI 会在结果产生后以 POST 请求把发票信息推送到该地址。这是获取开票结果的推荐方式。
回调类型
| callbackType | 触发时机 | response 内容 |
|---|---|---|
| 开票成功 | 蓝票开具成功(printState == 1) |
蓝票对象 |
| 开票失败 | 蓝票或红票开票失败(state == -1),可在控制台「开票参数」中关闭失败回调 |
发票对象,failMsg 为失败原因 |
| 红冲成功 | 红票开具成功 | 红票对象(color == red),parentInvoice 为原蓝票 |
| 作废成功 | 纸质发票作废成功(cancelState == 2) |
蓝票对象 |
| 作废失败 | 纸质发票作废失败 | 蓝票对象 |
| 审核驳回 | 控制台开启开票审核且审核驳回(auditState == -1) |
蓝票对象,consoleReason 为驳回原因 |
请求格式
POST {callbackUrl}?algorithm=HMAC-SHA256×tamp=1767225600
Content-Type: application/json;charset=UTF-8
invoice-sign: {签名}
{"callbackType":"开票成功","response":{...发票对象...},"responseType":"invoice"}
| 位置 | 名称 | 说明 |
|---|---|---|
| Query | algorithm | 签名算法,固定 HMAC-SHA256 |
| Query | timestamp | 回调发起时间,秒级时间戳 |
| Header | invoice-sign | 签名,见 签名校验 |
| Body | responseType | 对象类型:invoice 发票;平台型客户注册企业时为 shop(通知类型 商户审核结果) |
| Body | callbackType | 回调类型,见上表 |
| Body | response | 发票对象,字段与 查询发票详情 相同 |
业务系统应以 outOrderNo + color 识别发票
请求体中的 response.outOrderNo 为开票时传入的外部订单号,response.color 区分蓝票 / 红票。回调可能因重试重复送达,请做好幂等处理。
回调示例
{
"callbackType": "开票成功",
"response": {
"invoiceId": 10001,
"color": "blue",
"outOrderNo": "ORDER20260101001",
"category": "全电电子普通发票",
"state": 1,
"auditState": 1,
"printState": 1,
"cancelState": 0,
"redState": 0,
"statements": "已开票",
"price": 1000,
"tax": 56.6,
"purchaserName": "深圳市腾讯计算机系统有限公司",
"purchaserTaxpayerNumber": "91440300708461136T",
"printTime": "2026-01-01 10:00:35",
"allElectronicInvoiceNumber": "26312000000000000001",
"electronicInvoiceUrl": "https://.../invoice.pdf",
"electronicInvoiceOfd": "https://.../invoice.ofd",
"electronicInvoiceXml": "https://.../invoice.xml",
"downloadUrl": "https://.../download",
"failMsg": null,
"invoiceItems": [
{ "lineNumber": 1, "name": "*生产生活服务*技术服务费", "no": "3040201010000000000", "number": 1, "price": 1000, "sum": 1000, "taxRate": 0.06, "tax": 56.6 }
],
"...": "其余字段同查询发票详情"
},
"responseType": "invoice"
}
响应要求
业务系统收到回调后须返回 HTTP 200,且响应体为以下任一形式,EasyAPI 才认为回调成功:
- 纯文本
ok(不区分大小写); - JSON 且
code为1或200,例如{"code":1,"message":"ok"}。
其他响应(非 200、超时、响应体不符合要求)视为失败并进入重试。
重试规则
回调失败后按以下间隔重试,最多推送 8 次(首次 + 7 次重试),仍失败则该条回调标记为失效,可通过 手动触发回调 或 查询发票 补偿。
| 第几次重试 | 距上次间隔 |
|---|---|
| 1 | 2 分钟 |
| 2 | 10 分钟 |
| 3 | 10 分钟 |
| 4 | 1 小时 |
| 5 | 2 小时 |
| 6 | 6 小时 |
| 7 | 15 小时 |
同一张发票产生新结果时,尚未发送的旧回调会被删除,只推送最新状态。
签名校验
签名使用企业在控制台创建的 第一组密钥(按创建顺序)的 appKey / appSecret:
- 拼接签名串:
appKey={appKey}&algorithm=HMAC-SHA256×tamp={timestamp}&{path}&{body}{timestamp}为 Query 中的timestamp;{path}为回调地址的路径部分,例如回调地址https://www.example.com/invoice/callback?x=1的 path 为/invoice/callback;{body}为收到的请求体原文(JSON 字符串,不要重新序列化)。
- 以
appSecret为密钥对签名串做HMAC-SHA256,结果 Base64 编码。 - 与请求头
invoice-sign比较,一致则校验通过。
String message = "appKey=" + appKey
+ "&algorithm=HMAC-SHA256×tamp=" + timestamp
+ "&" + path
+ "&" + body;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(appSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String sign = Base64.getEncoder().encodeToString(mac.doFinal(message.getBytes(StandardCharsets.UTF_8)));
boolean valid = sign.equals(request.getHeader("invoice-sign"));
const crypto = require('node:crypto')
const message = `appKey=${appKey}&algorithm=HMAC-SHA256×tamp=${timestamp}&${path}&${rawBody}`
const sign = crypto.createHmac('sha256', appSecret).update(message, 'utf8').digest('base64')
const valid = sign === req.headers['invoice-sign']
注意
请使用请求体原始字符串参与签名。多数框架会先把 JSON 解析成对象,再序列化会改变字段顺序或数值格式,导致签名不一致。
手动触发回调
用于补偿:重新向指定地址推送某张发票的当前状态。
单张发票回调
GET
/invoice/callback| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| appKey | string | 条件必填 | 服务密钥 Key,与 appSecret 成对使用;传了 accessToken 时可不传 |
| appSecret | string | 条件必填 | 服务密钥 Secret |
| accessToken | string | 条件必填 | 访问令牌,通过 获取访问令牌 接口获取;传了 accessToken 则无需 appKey / appSecret |
| taxNumber | string | 必填 | 企业纳税人识别号,指定本次操作的开票企业;必须是该密钥已关联的企业 |
| outOrderNo | string | 必填 | 外部订单号 |
| color | string | 选填 | blue(默认)/ red |
| callbackUrl | string | 选填 | 回调地址,不传使用发票记录中的回调地址;传了会更新发票的回调地址 |
| callbackType | string | 选填 | 传 作废失败 时推送作废失败通知,不传按发票当前状态决定回调类型 |
{
"code": 1,
"message": "回调成功"
}
回调成功 表示回调任务已创建,实际推送异步进行。发票没有可推送的状态(例如仍在开票中)时不会产生回调。
批量回调
POST
/invoice/batch-callback| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| appKey | string | 条件必填 | 服务密钥 Key,与 appSecret 成对使用;传了 accessToken 时可不传 |
| appSecret | string | 条件必填 | 服务密钥 Secret |
| accessToken | string | 条件必填 | 访问令牌,通过 获取访问令牌 接口获取;传了 accessToken 则无需 appKey / appSecret |
| taxNumber | string | 必填 | 企业纳税人识别号,指定本次操作的开票企业;必须是该密钥已关联的企业 |
| ids | array | 条件必填 | 发票 ID 数组,与 outOrderNos 至少传一个,最多 5000 个 |
| outOrderNos | array | 条件必填 | 外部订单号数组,最多 5000 个 |
| callbackUrl | string | 选填 | 回调地址,不传使用各发票记录中的回调地址 |
| callbackType | string | 选填 | 同上 |
{
"appKey": "你的appKey",
"appSecret": "你的appSecret",
"taxNumber": "91320211MA1WML8X6T",
"outOrderNos": ["ORDER20260101001", "ORDER20260101002"],
"callbackUrl": "https://www.example.com/invoice/callback"
}
| message | 说明 |
|---|---|
| 商户订单号不能为空 | outOrderNo 为空 |
| 商户订单号蓝票不存在 / 红票不存在 | 发票不存在 |
| xxxURL格式不正确 | callbackUrl 不是合法 URL |
| 一次最多回调5000个发票 | 超出批量上限 |
| 暂无数据 | 批量回调没有匹配的发票 |