# API百宝箱 · 接口聚合开放平台 · 全文文档 > 北京开灵科技有限公司(开灵科技)接口聚合开放平台的完整接口文档全文。索引见 https://openapi.kailingteck.com/llms.txt。服务热线 010-60974119。 --- # 【发票查验】 # 发票查验接口 · AI 接入包 > 你是被客户委托来对接「发票查验接口」的 AI 助手。请严格按本文档,用客户指定的编程语言写出可运行的调用代码。 > 全部票种(数电/增值税/航空/铁路/区块链等)都用**这一个接口**,按票种传不同字段即可,系统自动路由。 ## 1. 接口地址 ``` POST https://openapi.kailingteck.com/api/v1/verify Content-Type: application/json ``` ## 2. 鉴权(每次请求必带三个头) - `X-App-Key`: 平台分配的 app_key - `X-Timestamp`: Unix 秒级时间戳(与服务器时差需 ≤300 秒,否则报防重放) - `X-Sign`: 签名 = `sha256(app_key + timestamp + 请求体原文 + app_secret)` 的小写十六进制 **签名务必注意**: 参与 sha256 的"请求体"必须和实际发送的 body **逐字节一致**(包括空格)。最稳的做法是:先把 body 序列化成一个字符串,签名和发送都用这同一个字符串。 签名参考(伪代码): ``` body = JSON.stringify(payload) // 固定下来这个字符串 ts = floor(now_seconds) sign = sha256_hex(app_key + ts + body + app_secret) ``` ## 3. 请求参数 | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | fphm | string | 必填 | 发票号码 | | fpdm | string | 条件 | 发票代码(数电票无代码可不传) | | kprq | string | 条件 | 开票日期 yyyy-MM-dd | | je | number | 条件 | 不含税金额(专用发票类必填) | | jym | string | 条件 | 校验码后 6 位(普通发票类必填;区块链发票为票面短码) | | jshj | number | 条件 | 价税合计(数电票 / 通用电子发票必填) | | xfsbh | string | 条件 | 销售方纳税人识别号(区块链 / 通用电子发票必填) | | dq | string | 条件 | 地区码(仅特殊票种需要,见地区码表) | | customReqId | string | 可选 | 调用方自定义请求 ID,便于对账与排障 | | cyList | array | 可选 | 批量查验:多张票字段放入数组(单次最多 50 张);不传则按上述扁平字段查单张 | ### 不同票种传哪些字段(照这张表传即可) | 票种 | 必填字段 | 地区码 dq | | --- | --- | --- | | 数电普通发票 | 发票号码 + 开票日期 + 价税合计 | 不用填 | | 数电专用发票 | 发票号码 + 开票日期 + 价税合计 | 不用填 | | 数电纸质(专用发票) | 发票代码 + 发票号码 + 开票日期 + 不含税金额 | 不用填 | | 数电纸质(普通发票) | 发票代码 + 发票号码 + 开票日期 + 校验码后6位 | 不用填 | | 增值税专用发票 | 发票代码 + 发票号码 + 开票日期 + 不含税金额 | 不用填 | | 增值税普通发票 / 电子普票 / 通行费 | 发票代码 + 发票号码 + 开票日期 + 校验码后6位 | 不用填 | | 区块链发票(深圳/北京/云南) | 发票代码 + 发票号码 + 销方税号 + 校验码 | 4403 / 1100 / 5300 | | 通用电子发票(广东/浙江宁波) | 发票代码 + 发票号码 + 销方税号 + 价税合计 | 4400 / 3300 | | 车辆通行费发票(江苏) | 发票代码 + 发票号码 + 开票日期 + 不含税金额 | 3200 | ### 地区码 dq(仅特殊票种需要) | dq | 地区 | 适用 | | --- | --- | --- | | (不传) | 全国 | 增值税 / 数电 / 数电纸质发票,默认通道 | | 4403 | 深圳 | 区块链发票 | | 1100 | 北京 | 区块链发票 | | 5300 | 云南 | 区块链发票 | | 4400 | 广东 | 通用电子发票 | | 3300 | 浙江 / 宁波 | 通用电子发票 | | 3200 | 江苏 | 车辆通行费发票 | ## 4. 响应结构 判断是否查验通过,**看 `data.verified`**(true=真票且要素一致)。不要只看外层 code。 ### 公共字段(所有票种一致) | 字段 | 中文名 | 类型 | 说明 | | --- | --- | --- | --- | | code | 业务编码 | string | CYT_00000 为成功,其余见业务编码表 | | msg | 提示信息 | string | | | requestId | 请求标识 | string | 排障可提供 | | data.verified | 是否查验通过 | boolean | 真票且要素一致 | | data.channel | 路由通道 | string | 本次自动选用的查验通道 | | data.result | 逐票结果 | array | 每张票一项 | | result[].success | 该票是否成功 | boolean | | | result[].fphm | 发票号码 | string | | | result[].kprq | 开票日期 | string | | | result[].fplx | 发票类型代码 | string | 见附录 | | result[].times | 当日查验次数 | integer | | | result[].data | 全票面明细 | object | 字段随票种不同,见下表 | ### 票面明细 result[].data(按票种,字段不同) **增值税 / 数电发票(普票·专票·数电纸质)** | 字段 | 中文名 | 类型 | | --- | --- | --- | | xfmc | 销售方名称 | string | | xfsbh | 销售方税号 | string | | gfmc | 购买方名称 | string | | gfsbh | 购买方税号 | string | | je | 不含税金额 | number | | se | 税额 | number | | jshj | 价税合计 | number | | jshjcn | 价税合计(大写) | string | | fpztDm | 发票状态代码 | string | | bz | 备注 | string | | hwxx[] | 货物/服务明细 | array | | hwxx[].mc | 货物或服务名称 | string | | hwxx[].slv | 税率 | number | | hwxx[].se | 明细税额 | number | | hwxx[].spbm | 税收分类编码 | string | **航空运输电子客票行程单** —— 税额字段是 zzsse(不是 se);税费拆得很细 | 字段 | 中文名 | 类型 | | --- | --- | --- | | lkxm | 旅客姓名 | string | | sfzjhm | 证件号码 | string | | pj | 票价 | number | | ryfjf | 燃油附加费 | number | | zzssl | 增值税税率 | string | | zzsse | 增值税税额 | number | | mhfzjj | 民航发展基金 | number | | qtsf | 其他税费 | number | | bxf | 保险费 | number | | jshj | 合计 | number | | fpztDm | 发票状态代码 | string | | hwxx[] | 航段明细 | array | | hwxx[].cyr | 承运人 | string | | hwxx[].hbh | 航班号 | string | | hwxx[].sfz | 始发站 | string | | hwxx[].mdz | 目的站 | string | | hwxx[].qfsj | 起飞时间 | string | | hwxx[].zwdj | 座位等级 | string | **铁路电子客票** —— 旅客字段是 name(非航空票的 lkxm);税额是 se;车次/席别为平铺字段、无 hwxx | 字段 | 中文名 | 类型 | | --- | --- | --- | | name | 旅客姓名 | string | | zjh | 证件号码 | string | | cc | 车次 | string | | cfz | 出发站 | string | | ddz | 到达站 | string | | ccrq | 乘车日期 | string | | cfsj | 出发时间 | string | | xw | 席别 | string | | je | 不含税金额 | number | | se | 税额 | number | | slv | 税率 | string | | jshj | 价税合计(票价) | number | | fpztDm | 发票状态代码 | string | **区块链发票(深圳/北京/云南)** —— 局端只验真,字段较少;qkl=true 标识区块链票 | 字段 | 中文名 | 类型 | | --- | --- | --- | | xfmc | 销售方名称 | string | | xfsbh | 销售方税号 | string | | gfmc | 购买方名称 | string | | je | 不含税金额 | number | | se | 税额 | number | | jshj | 价税合计 | number | | jym | 校验码 | string | | fpzt | 发票状态 | string | | qkl | 区块链标识 | boolean | > ⚠️ 注意各票种"税额"字段名不同:普通/增值税/铁路票是 `se`,航空客票是 `zzsse`。 ## 5. 业务编码 | code | 含义 | | --- | --- | | CYT_00000 | 查验成功 / 真票 | | CYT_10001 | 查无此票 | | CYT_10002 | 票面要素与底账不一致 | | CYT_40001 | 请求参数缺失或格式错误 | | CYT_40100 | 缺少鉴权请求头 | | CYT_40101 | app_key 不存在或已停用 | | CYT_40102 | 签名校验失败 | | CYT_40103 | 请求时间戳过期(防重放) | | CYT_42900 | 配额已用尽 | | CYT_50200 | 查验源暂时不可用,请稍后重试 | ## 6. 失败处理(是否重试) | 情况 | 是否重试 | 说明 | | --- | --- | --- | | 查验源/税局服务异常(CYT_50200) | 可重试 | 多为短时波动,建议 15–20 分钟后再试;月底税局停机维护也会偶发 | | 请求时间戳过期(CYT_40103) | 可重试 | 校准本地时间后重新签名发起即可 | | 查无此票(CYT_10001) | 不要重试 | 可能离线开票数据未上传,或要素有误;反复查会占用税局当日查验次数 | | 要素不一致(CYT_10002) | 不要重试 | 核对四要素是否与票面一致,金额是否为不含税金额 | | 参数错误(CYT_40001) | 不要重试 | 按票种传参对照表核对必填字段 | | 签名/鉴权失败(CYT_40102/40101) | 不要重试 | 检查 app_key/app_secret 与签名算法 | | 配额已用尽(CYT_42900) | 不要重试 | 联系商务续费或调高配额 | ## 7. 请求示例(数电普票) ```json POST https://openapi.kailingteck.com/api/v1/verify { "fphm": "26112000002558759236", "kprq": "2026-06-23", "jshj": 377.00 } ``` --- ## 给 AI 的任务 请用客户指定的语言(如未指定则用客户项目的主语言),实现一个 `verifyInvoice(fields)` 函数: 1. 按上面"鉴权"算好 X-Timestamp 与 X-Sign; 2. POST 到接口地址,带三个鉴权头; 3. 解析响应,以 `data.verified` 判断真伪,失败时读 `data.reason`; 4. 票面明细按票种从 `data.result[0].data` 取(税额注意 se / zzsse 区别); 5. 失败按第 6 节决定是否重试。 把 app_key / app_secret 留成配置项,不要硬编码。 --- # 【企业抬头与税号查询】 # 企业抬头与税号查询 · AI 接入包 > 你是被客户委托来对接「企业抬头与税号查询」接口的 AI 助手。请严格按本文档写出可运行的对接代码。 ## 1. 这个接口干什么 **名字查税号、税号查名字,两个方向都支持**,统一返回企业全称 + 纳税人识别号。 - 用途一(最常见):开票抬头**自动补全**——用户敲「北京数影」,下拉列出候选,选中后同时拿到全称和税号,直接填进发票抬头,省得手输错。 - 用途二:手里已有税号,要**回填抬头或做校验**——直接用税号查,返回那一家的全称。 ## 2. 接口地址与鉴权 ``` POST https://openapi.kailingteck.com/api/v1/enterprise/search Content-Type: application/json ``` 三个请求头(和开灵其它接口完全一样): | 请求头 | 值 | | --- | --- | | `X-App-Key` | 开灵签发的 app_key | | `X-Timestamp` | Unix **秒级**时间戳(±300 秒内) | | `X-Sign` | `sha256(app_key + timestamp + 请求体原文 + app_secret)` 小写十六进制 | 参与签名的 body 必须与实际发送的**逐字节一致**:先序列化成一个字符串变量,签名和发送都用它。 ## 3. 请求参数 **一个接口两种查法,地址和鉴权完全一样,区别只在于传哪个字段。**`name` 与 `tax_id` **二选一**,不能同时传、也不能都不传,否则返回 400。 | 字段 | 必填 | 说明 | | --- | --- | --- | | `name` | 二选一 | **按名称模糊查**:企业名称关键词,**2~64 个字符**。3 字以上支持跨词匹配(「数影科技」能命中「北京数影互联科技有限公司」) | | `tax_id` | 二选一 | **按税号精确查**:完整纳税人识别号,**15~20 位数字或字母**。小写会自动转大写,不用自己处理 | | `limit` | 选填 | 最多返回几条,1~30,默认 30。按税号查只会返回 1 条,该字段用不上 | 按名称模糊查: ```json { "name": "北京数影", "limit": 10 } ``` 按税号精确查: ```json { "tax_id": "91110105MA01ECN70Q" } ``` ## 4. 返回 | 字段 | 说明 | | --- | --- | | `mode` | 本次走的是哪种查法:`name`=按名称模糊查 / `tax_id`=按税号精确查 | | `total` | 本次返回条数。按税号查命中为 1,查不到为 0 | | `list[].company_name` | 企业全称 | | `list[].tax_id` | 纳税人识别号(统一社会信用代码) | | `cached` | true=命中了平台缓存(结果一样,只是没再打上游) | | `isMock` | true=演示数据,不是真实工商数据 | ```json { "code": "CYT_00000", "msg": "success", "requestId": "cyt_xxx", "data": { "mode": "name", "total": 2, "list": [ { "tax_id": "91110112567417501L", "company_name": "北京数影创艺数字科技有限公司" }, { "tax_id": "91110105MA01ECN70Q", "company_name": "北京数影互联科技有限公司" } ], "cached": false, "isMock": false } } ``` 按税号精确查的返回(实测): ```json { "code": "CYT_00000", "msg": "success", "data": { "mode": "tax_id", "total": 1, "list": [ { "tax_id": "91110105MA01ECN70Q", "company_name": "北京数影互联科技有限公司" } ], "cached": false, "isMock": false } } ``` 查不到时 `total` 为 0、`list` 为空数组,**这是正常返回、不是错误**。 ## 5. 错误码 | HTTP / code | 含义与处理 | | --- | --- | | 400 · CYT_40001 | 三种情况:`name` 与 `tax_id` 都没传/两个都传了/格式不对(`name` 不在 2~64 字,或 `tax_id` 不是 15~20 位)。读 `data.hint` | | 401 · CYT_40100 / 40102 | 缺鉴权头或签名错 | | 401 · CYT_40103 | 时间戳超出 ±300 秒 | | 403 · CYT_40301 | 该密钥不是本产品的 | | 429 · CYT_42901 | 超过每分钟 120 次限流 | | 502 · CYT_50200 | 企业信息源暂时不可用(平台已自动重试过一次),稍后重试即可 | ## 6. ⚠ 做输入联想时必看(否则费用会失控) **本接口按调用次数计费,客户调一次算一次。** 如果你要实现"边打字边联想",一定要做这三件事,否则用户填一次表可能触发几十次调用: 1. **防抖**:输入停止 **300 毫秒**后再发请求,不要每敲一个字就发 2. **少于 2 个字不查**:上游要求 name ≥ 2 字,少于 2 字直接不发请求 3. **前端也留一层缓存**:同一个词已经查过就别再查 平台侧已有 10 分钟服务端缓存(省的是上游额度),但**对客户仍按调用次数计费**,所以前端防抖是省钱的关键。 ```js let timer, lastQuery = '', memo = new Map(); input.oninput = () => { const kw = input.value.trim(); clearTimeout(timer); if (kw.length < 2 || kw === lastQuery) return; // 少于2字/重复词不查 if (memo.has(kw)) return render(memo.get(kw)); // 前端缓存 timer = setTimeout(async () => { // 防抖 300ms lastQuery = kw; const r = await searchEnterprise(kw); memo.set(kw, r.data.list); render(r.data.list); }, 300); }; ``` ## 7. 给 AI 的任务 1. 写一个 `searchEnterprise(name, limit)` 方法,封装第 2 节的签名逻辑,app_key/app_secret 作为配置项,**app_secret 只能放服务端**。 2. 若客户要做输入联想,按第 6 节实现防抖 + 最小字数 + 前端缓存三件套,**不要省略**。 3. 查不到结果时返回空列表并允许用户手动输入,**不要阻塞用户填表**。 4. 502 时静默降级为手输,不要弹错误框打断用户。 --- # 【正向开票】 # 正向开票 · 腾讯联用 — AI 接入包 > **开票数据真实性与税务责任** > 发票的全部票面要素——购销方名称与纳税人识别号、商品名称与税收分类编码、数量金额与税率、支付流水号等——必须真实,并与实际发生的业务一致。 > 开灵仅提供接口通道与报文转发,不对报文内容做真实性核验。 > 若因填报虚假、编造或与实际业务不符的数据(例如编造的支付流水号、不实的购方税号、与真实交易不符的商品名称或税收分类编码)导致发票异常、被税务机关核查或稽查,相关法律责任与后果由开票方(销方)自行承担。 > 生成代码时不要为 `pay_info`、购方税号等字段填充占位或随机值——没有真实值就留空。 > 你是被客户委托来对接「乐企联用正向开票」的 AI 助手。请严格按本文档写出可运行的对接代码,不要臆造字段。 ## 0. 一句话架构 客户业务系统 → 调开灵这 4 个接口 → 开灵中台 → 开票通道 → 税务局。 开票通道那套复杂签名(HMAC-SHA256、双分隔符)、税盘绑定、票文件转存,**全部由开灵中台承担**,客户只需按第 1 节做一次简单签名。 **接入前提**:贵司税号由开灵完成税盘绑定后即可开票,**你不需要调用任何开通接口**。若开票报「未绑定」类错误,联系开灵项目经理。 ## 1. 鉴权与签名(所有接口一样) | 请求头 | 值 | | --- | --- | | `X-App-Key` | 开灵签发的 app_key | | `X-Timestamp` | Unix **秒级**时间戳(±300 秒内) | | `X-Sign` | `sha256(app_key + timestamp + 请求体原文 + app_secret)` 小写十六进制 | 参与签名的 body 必须与实际发送的**逐字节一致**:先序列化成一个字符串变量,签名和发送都用它。 ```js const body = JSON.stringify(payload); const ts = Math.floor(Date.now() / 1000).toString(); const sign = sha256hex(appKey + ts + body + appSecret); fetch(url, { method:'POST', headers:{ 'Content-Type':'application/json','X-App-Key':appKey,'X-Timestamp':ts,'X-Sign':sign }, body }); ``` ## 2. 返回信封 ```json { "code": "CYT_00000", "msg": "success", "requestId": "cyt_xxx", "data": { } } ``` ⚠ **`code=CYT_00000` 只代表调用成功,不代表票开出来了。** 开票是异步的,必须用②查结果看 `data.status` 是否为 3。 ## 3. 调用顺序 ``` ① 开票(拿 order_sn) → 轮询 ② 查结果,直到 status=3 开票成功 → 从返回里直接拿 pdf_url / ofd_url / xml_url 交付给用户 【按需】③ 红冲 【月底】📊 统计对账 ``` **关键值 `order_id` 由你生成、全局唯一**,查结果和红冲都靠它。 ## 4. 接口详情(4 个) ### ① 开票 开一张数电发票。所有业务共用这一个接口——普通商品直接调,属于税局认定的特定业务时在基础字段之外多传一块即可。开票是异步的:本接口只返回受理号,票要用②查结果拿。 `POST https://openapi.kailingteck.com/api/v1/invoice/forward/create` **请求参数** 按业务场景分开列,见下方「特定业务字段」。不涉及特定业务的普通开票看「基础开票」那一块。以下是场景无关的关键字段: | 字段 | 必填 | 说明 | | --- | --- | --- | | `order_id` | 必填 | 你侧订单号,全局唯一。查结果、红冲都用它,不要复用 | | `seller_taxpayer_num` | 必填 | 销方税号(开票方,即贵司税号)。★须与实际业务一致,虚假填报引发的税务责任由开票方承担。 | | `terminal_code` | 必填 | 数电票规则:传销方税号,不是传统税盘号 | | `invoice_type_code` | 必填 | 082 全电电子普票 / 081 全电电子专票 | | `title_type` | 必填 | 抬头类型:1 个人或政府事业单位 / 2 企业 | | `buyer_title` | 必填 | 购方名称。title_type=2 时 buyer_taxpayer_num 也必填。★须为真实购买方,不得虚构或代填他人抬头。 | | `buyer_taxpayer_num` | 条件 | 购方税号,抬头类型为企业时必填。★须与实际业务一致,虚假填报引发的税务责任由开票方承担。 | | `drawer` | 必填 | 开票人姓名,需与电子税务局登记一致。★不得填写他人姓名。 | | `amount_has_tax` | 必填 | 含税总金额,元,两位小数。★须等于实际成交金额,★须与实际业务一致,虚假填报引发的税务责任由开票方承担。 | | `amount_without_tax` | 必填 | 不含税总金额 | | `tax_amount` | 必填 | 总税额。★须与金额、税率自洽(不含税金额+税额=含税金额),不得凑数。 | | `items[].name` | 必填 | 商品名称,50 字内。不含 < > / & 等特殊符号。部分特定业务对名称写法另有要求,见下方对应的业务块。★须如实反映所售商品或服务,不得以其他品名替代。 | | `items[].tax_code` | 必填 | 税收分类编码,见税局分类编码表。★须与商品或服务的实际属性匹配;拿不准的先与主管税务机关或开灵确认,不要试错。编码错配引发的税务责任由开票方承担。 | | `items[].total_price` | 必填 | 该行不含税金额 = 含税金额 ÷(1+税率),两位小数 | | `items[].tax_rate` | 必填 | 税率,最多三位小数。★须为该商品或服务适用的法定税率,不得自行调整。 | | `items[].tax_amount` | 必填 | 该行税额 = 不含税金额 × 税率,两位小数 | | `items[].total` | 条件 | 数量,最多 8 位小数。传了 price 就必填 | | `items[].price` | 条件 | 不含税单价 = 不含税金额 ÷ 数量。传了 total 就必填 | | `items[].unit` | 选填 | 商品单位,显示在票面「单位」栏,可填「个」「次」等。部分特定业务对单位取值有强制要求,见下方对应的业务块 | | `items[].models` | 选填 | 规格型号,显示在票面「规格型号」栏 | | `items[].discount` | 选填 | 含税折扣总金额,必须是负数 | | `items[].preferential_policy_flag` | 选填 | 优惠政策标志 | | `items[].zero_tax_flag` | 选填 | 零税率标识 | | `pay_info[]` | 选填 | 支付信息集合。★开灵实测:不传也能开出票。但如果你的业务有真实线上支付环节(尤其成品油「支付即开票」),建议如实传——通道文档要求乐企联用传真实渠道流水号,编造的流水号在支付与发票匹配核验时有风险 | | `pay_info[].pay_num` | 选填 | ★真实的渠道交易流水号(微信支付流水号以 420 开头)。没有线上支付环节就留空,不要编一个填进去——虚构流水号引发的税务风险由开票方承担 | | `pay_info[].pay_channel_code` | 选填 | 交易渠道代码:001 现金 / 002 银行转账 / 003 票据 / 004 借记卡 / 005 信用卡 / 006 购物卡券 / 007 优惠券 / 008 商场积分 / 009 支付宝 / 010 微信支付 / 011 云闪付 / 012 Apple Pay / 013 Samsung Pay / 099 其他。★通道文档写明乐企联用只支持微信支付(010);实测传 001 现金也能开,但以真实支付方式为准 | | `callback_url` | 必填 | ★开票结果回传地址——回调不用另外配置,就是随这个请求送过去的。开票状态一变,平台会 POST 推到这个地址;推送字段见下方「开票结果回调」块。不想轮询就靠它。红冲的回传地址在 ③ 的 invoices[].callback_url 里单独传 | | `specific_business_code` | 特定业务必填 | ★特定业务代码——这是通往「特定业务」的入口。普通开票不填;做税局认定的特定业务时填对应代码,并在请求体顶层加上该代码对应的字段块。取值:01 成品油 / 03 建筑服务 / 04 货物运输 / 05 不动产销售 / 06 不动产经营租赁 / 09 旅客运输 / 13 拖拉机和联合收割机 / 15 数电二手车 / 51、52 二手车销售统一发票。★农产品收购(16)、自产农产品(12)、报废产品收购(24)不在本产品范围——这三类是收购方开给出售方的票,属于反向开票,请用平台的「反向开票」产品 | | `special_invoice_kind` | 选填 | 特殊票种标识,普通开票传空或 00。做特定业务时按业务取值,具体取值见下方对应的业务块 | | `e_extra_info` | 选填 | 特殊行业扩展信息,传 JSON 字符串(不是对象)。医疗服务住院(specific_business_code=10)/门诊(=11)时传病人信息;乐企微信支付拆分金额时传支付订单数据 | **请求示例** ```json { "order_id": "REG1788017096448", "invoice_type_code": "082", "seller_taxpayer_num": "91110119MABQDU249D", "seller_name": "北京湛露科技有限公司", "seller_address": "北京市", "seller_tel": "01000000000", "seller_bank_name": "测试银行", "seller_bank_account": "1234567890", "title_type": 1, "buyer_title": "个人", "terminal_code": "91110119MABQDU249D", "drawer": "开灵测试", "callback_url": "https://你们的域名/invoice/callback", "template": 1, "amount_has_tax": "1.00", "amount_without_tax": "0.88", "tax_amount": "0.12", "taxpayer_confirmation": { "draw_with_nobuyer_flag": 1 }, "items": [ { "name": "办公用品", "tax_code": "1080118000000000000", "unit": "个", "total": "1", "price": "0.88", "total_price": "0.88", "tax_rate": "0.13", "tax_amount": "0.12", "amount_has_tax": "1.00" } ] } ``` **按业务场景分开的完整参数** 公共字段填完后,若属于税局认定的特定业务,还要在**同一个请求体**里补两样:① 把 `specific_business_code` 填成本行业务代码;② 在**请求体顶层**(与 `items` 平级,不是塞进 items 里)加上该业务对应的对象。注意其中差额征税、销方/购方证件信息、冠名发票这 4 块**不由** `specific_business_code` 触发,各自触发条件见块内说明。 **一、特定业务(由 `specific_business_code` 决定)** —— 填了哪个业务代码,就在请求体顶层加上对应的这一块。 #### 成品油 > 什么时候传:加油站、油品销售开票时 > > **实测**:⚠️ 报文格式已实测通过(通道受理成功),但税局要求开票企业具备成品油经销或生产资质,测试主体不具备故未出票。有资质的企业照此报文可直接开 > > **触发条件**:业务代码 specific_business_code = 01(成品油),同时特殊票种 special_invoice_kind = 08(成品油) > > **加在哪里**:★没有独立的对象块。成品油靠两个标志位 + items[] 里的常规字段表达,不需要在顶层加任何新对象 | 字段 | 必填 | 说明 | | --- | --- | --- | | `specific_business_code` | 是 | 固定传 "01"(成品油)。★这是税局的特定业务代码,不传会被拒 | | `special_invoice_kind` | 是 | 固定传 "08"(成品油)。★与上面那个是两套体系的字段,成品油要同时传 | | `items[].unit` | 是 | ★单位只能是 LITER(升)或 TON(吨)——这两个值是开灵实测确认的英文枚举。厂商文档写的是中文「升」「吨」且只写在红字信息表接口上,蓝票接口没写约束,以实测为准 | | `items[].name` | 是 | 油品名称。建议按规范油品名填写(如 92号车用汽油、0号车用柴油),厂商文档对此无强制约束文字 | | `items[].tax_code` | 是 | 税收分类编码:汽油 1070101010100000000、柴油 1070101030100000000、液化石油气 1070101110100000000 | | `items[].tax_rate` | 是 | 汽油柴油 0.13;液化石油气 0.09 | | `items[].total` | 是 | 数量,即加油升数或吨数(最多 8 位小数) | | `(其余字段)` | — | 与普通开票完全一致,照基础字段填即可 | 完整请求报文(公共字段已合并,改值即可直接调): ```json { "order_id": "ORD20260829001", "invoice_type_code": "082", "seller_taxpayer_num": "91110119MABQDU249D", "seller_name": "北京湛露科技有限公司", "seller_address": "北京市", "seller_tel": "01000000000", "seller_bank_name": "测试银行", "seller_bank_account": "1234567890", "title_type": 1, "buyer_title": "个人", "terminal_code": "91110119MABQDU249D", "drawer": "开灵测试", "callback_url": "https://你们的域名/invoice/callback", "template": 1, "amount_has_tax": "90.40", "amount_without_tax": "80.00", "tax_amount": "10.40", "taxpayer_confirmation": { "draw_with_nobuyer_flag": 1 }, "items": [ { "name": "92号车用汽油", "tax_code": "1070101010100000000", "unit": "LITER", "total": "13.5", "price": "5.925926", "total_price": "80.00", "tax_rate": "0.13", "tax_amount": "10.40", "amount_has_tax": "90.40" } ], "specific_business_code": "01", "special_invoice_kind": "08" } ``` #### 建筑服务 > 什么时候传:做建筑服务、要写明施工地和项目名时 > > **实测**:✅ 已实测开出发票(票号 26117200000004866583) > > **触发条件**:业务代码 specific_business_code = 03(建筑服务行业) > > **加在哪里**:请求体顶层,与 items 平级(不是塞在 items 里) | 字段 | 必填 | 说明 | | --- | --- | --- | | `building_service` | 否 | **开具建筑服务类全电发票(数电票)时必填** | | `building_service.site` | 是 | 建筑服务发生地 | | `building_service.detail_site` | 否 | 发生地详细地址 | | `building_service.building_project` | 是 | 建筑项目名称 | | `building_service.cross_sign` | 是 | 跨地市标志,填值:Y、N | | `building_service.vat_land_number` | 否 | 土地增值税项目编号 | | `building_service.cross_confirm_flag` | 否 | Y/N | | `building_service.cross_num` | 否 | 跨区域涉税事项报验管理编号 ,只有跨地区的时候需要必填 | 完整请求报文(公共字段已合并,改值即可直接调): ```json { "order_id": "ORD20260829001", "invoice_type_code": "082", "seller_taxpayer_num": "91110119MABQDU249D", "seller_name": "北京湛露科技有限公司", "seller_address": "北京市", "seller_tel": "01000000000", "seller_bank_name": "测试银行", "seller_bank_account": "1234567890", "title_type": 1, "buyer_title": "个人", "terminal_code": "91110119MABQDU249D", "drawer": "开灵测试", "callback_url": "https://你们的域名/invoice/callback", "template": 1, "amount_has_tax": "109.00", "amount_without_tax": "100.00", "tax_amount": "9.00", "taxpayer_confirmation": { "draw_with_nobuyer_flag": 1 }, "items": [ { "name": "*建筑服务*工程款", "tax_code": "3040101000000000000", "unit": "项", "total": "1", "price": "100.00", "total_price": "100.00", "tax_rate": "0.09", "tax_amount": "9.00", "amount_has_tax": "109.00" } ], "specific_business_code": "03", "building_service": { "site": "北京市大兴区", "detail_site": "北京市大兴区庞各庄镇示例路 1 号", "building_project": "示例产业园二期土建工程", "cross_sign": "N" } } ``` #### 货物运输 > 什么时候传:做货物运输、开运输服务发票时 > > **实测**:✅ 已实测开出发票(票号 26117200000004866584) > > **触发条件**:业务代码 specific_business_code = 04(货物运输) > > **加在哪里**:请求体顶层,与 items 平级(不是塞在 items 里) | 字段 | 必填 | 说明 | | --- | --- | --- | | `goods_transport` | 否 | **开具货物运输服务全电发票(数电票)时必填** | | `goods_transport.transport_number` | 否 | 运输工具号牌,运输工具种类属于无牌号的工具种类时,需填写无 | | `goods_transport.transport_kind` | 否 | 运输工具种类: | | `goods_transport.goods_name` | 否 | 货物运输名称 | | `goods_transport.departure_place` | 否 | 出发地.按行政区划名称填写,包括县(市、区)名称,可在县(市、区)后自主增添具体地址 | | `goods_transport.destination_place` | 否 | 到达地,与出发地同要求 | 完整请求报文(公共字段已合并,改值即可直接调): ```json { "order_id": "ORD20260829001", "invoice_type_code": "082", "seller_taxpayer_num": "91110119MABQDU249D", "seller_name": "北京湛露科技有限公司", "seller_address": "北京市", "seller_tel": "01000000000", "seller_bank_name": "测试银行", "seller_bank_account": "1234567890", "title_type": 1, "buyer_title": "个人", "terminal_code": "91110119MABQDU249D", "drawer": "开灵测试", "callback_url": "https://你们的域名/invoice/callback", "template": 1, "amount_has_tax": "109.00", "amount_without_tax": "100.00", "tax_amount": "9.00", "taxpayer_confirmation": { "draw_with_nobuyer_flag": 1 }, "items": [ { "name": "*运输服务*运输服务费", "tax_code": "3010101010100000000", "unit": "次", "total": "1", "price": "100.00", "total_price": "100.00", "tax_rate": "0.09", "tax_amount": "9.00", "amount_has_tax": "109.00" } ], "specific_business_code": "04", "goods_transport": [ { "transport_number": "京A12345", "transport_kind": "公路运输", "goods_name": "钢材", "departure_place": "北京市大兴区", "destination_place": "天津市滨海新区" } ] } ``` #### 不动产销售 > 什么时候传:销售不动产开发票时 > > **实测**:✅ 已实测开出发票(票号 26117200000004866586) > > **触发条件**:业务代码 specific_business_code = 05(不动产销售服务) > > **加在哪里**:请求体顶层,与 items 平级(不是塞在 items 里) | 字段 | 必填 | 说明 | | --- | --- | --- | | `real_static_sales` | 否 | **开具不动产销售发票(数电票)时必填** | | `real_static_sales.property_num` | 否 | 房屋产权证书/不动产权证号码 | | `real_static_sales.loaction` | 是 | 不动产地址 | | `real_static_sales.address_detail` | 否 | 不动产详细地址 | | `real_static_sales.cross_sign` | 是 | 跨市标志,填值:Y、N | | `real_static_sales.unit_of_area` | 是 | 面积单位 : | | `real_static_sales.online_contract_code` | 否 | 不动产单元代码/网签合同备案编码 | | `real_static_sales.vat_land_number` | 否 | 土地增值税项目编号 | | `real_static_sales.assessed_taxable_amount` | 否 | 核定计税价格 | | `real_static_sales.actual_taxable_amount` | 否 | 实际成交含税金额 | 完整请求报文(公共字段已合并,改值即可直接调): ```json { "order_id": "ORD20260829001", "invoice_type_code": "082", "seller_taxpayer_num": "91110119MABQDU249D", "seller_name": "北京湛露科技有限公司", "seller_address": "北京市", "seller_tel": "01000000000", "seller_bank_name": "测试银行", "seller_bank_account": "1234567890", "title_type": 1, "buyer_title": "个人", "terminal_code": "91110119MABQDU249D", "drawer": "开灵测试", "callback_url": "https://你们的域名/invoice/callback", "template": 1, "amount_has_tax": "109.00", "amount_without_tax": "100.00", "tax_amount": "9.00", "taxpayer_confirmation": { "draw_with_nobuyer_flag": 1 }, "items": [ { "name": "*不动产*商业用房", "tax_code": "5010202000000000000", "unit": "平方米", "total": "1", "price": "100.00", "total_price": "100.00", "tax_rate": "0.09", "tax_amount": "9.00", "amount_has_tax": "109.00" } ], "specific_business_code": "05", "real_static_sales": { "property_num": "京(2026)大兴区不动产权第0001号", "loaction": "北京市大兴区", "address_detail": "北京市大兴区庞各庄镇示例路 1 号", "cross_sign": "N", "unit_of_area": 20 } } ``` #### 不动产租赁 > 什么时候传:出租不动产(含停车费)开租赁发票时 > > **实测**:✅ 已实测开出发票(票号 26117200000004866585)。★停车场编码下 car_num 车牌号是硬要求,不传会被退 > > **触发条件**:业务代码 specific_business_code = 06(不动产经营租赁服务) > > **加在哪里**:请求体顶层,与 items 平级(不是塞在 items 里) | 字段 | 必填 | 说明 | | --- | --- | --- | | `real_static_rent` | 否 | **开具不动产经营租赁服务全电发票(数电票)时必填** | | `real_static_rent.property_num` | 是 | 房屋产权证书/不动产权证号码, 若没有证书填写 "无" | | `real_static_rent.location` | 是 | 不动产地址 (省市区县) | | `real_static_rent.address_detail` | 是 | 不动产详细地址,必须包含“街、路、村、乡、镇、道、巷、号”等关键词之一 | | `real_static_rent.rent_begin_date` | 是 | 租赁期起 统一传YYYYMMDDhhmm格式 ,其中除停车费(税收分类编码为3040502020200000000)开票项外,开具其他的税收编码项会自动截取为YYYYMMDD | | `real_static_rent.rent_end_date` | 是 | 租赁期止 统一传YYYYMMDDhhmm格式 , 其中除停车费(税收分类编码为3040502020200000000)开票项外,开具其他的税收编码项会自动截取为YYYYMMDD | | `real_static_rent.intercity_flag` | 是 | 跨地市标志 Y-是 N-否 | | `real_static_rent.unit_of_area` | 是 | 面积单位 10:平方千米; 20:平方米; 30:公顷; 40:亩 | | `real_static_rent.car_num` | 否 | 车牌号:乐企方式开停车发票(税收分类编码为3040502020200000000)必填 ,其他的方式开停车非必填,填了就会显示内容; | 完整请求报文(公共字段已合并,改值即可直接调): ```json { "order_id": "ORD20260829001", "invoice_type_code": "082", "seller_taxpayer_num": "91110119MABQDU249D", "seller_name": "北京湛露科技有限公司", "seller_address": "北京市", "seller_tel": "01000000000", "seller_bank_name": "测试银行", "seller_bank_account": "1234567890", "title_type": 1, "buyer_title": "个人", "terminal_code": "91110119MABQDU249D", "drawer": "开灵测试", "callback_url": "https://你们的域名/invoice/callback", "template": 1, "amount_has_tax": "109.00", "amount_without_tax": "100.00", "tax_amount": "9.00", "taxpayer_confirmation": { "draw_with_nobuyer_flag": 1 }, "items": [ { "name": "*经营租赁*车辆停放服务", "tax_code": "3040502020200000000", "unit": "个", "total": "1", "price": "100.00", "total_price": "100.00", "tax_rate": "0.09", "tax_amount": "9.00", "amount_has_tax": "109.00" } ], "specific_business_code": "06", "real_static_rent": { "property_num": "无", "location": "北京市大兴区", "address_detail": "北京市大兴区庞各庄镇示例路 1 号", "rent_begin_date": "202608010000", "rent_end_date": "202612310000", "intercity_flag": "N", "unit_of_area": 20, "car_num": "京A12345" } } ``` #### 旅客运输 > 什么时候传:做旅客运输、要把行程开进发票时 > > **实测**:✅ 已实测开出发票(票号 26117200000004866587) > > **触发条件**:业务代码 specific_business_code = 09(旅客运输行业) > > **加在哪里**:请求体顶层,与 items 平级(不是塞在 items 里) | 字段 | 必填 | 说明 | | --- | --- | --- | | `passenger_transport` | 否 | **开具旅客运输行业开全电发票(数电票)时必填** | | `passenger_transport.traveler` | 否 | 出行人 | | `passenger_transport.id_type` | 否 | 出行人证件类型 102营业执照、201居民身份证、202军官证、203武警警官证、204士兵证、206残疾人证、207残疾军人证(1-8级)、208外国护照、210港澳居民来往内地通行证、212中华人民共和国往来港澳通行证、213台湾居民来往大陆通行证、214大陆居民往来台湾通行证、215外国人居留 | | `passenger_transport.id_number` | 否 | 出行人证件号码 | | `passenger_transport.travel_date` | 否 | 出行日期 yyyy-MM-dd格式 | | `passenger_transport.departure_place` | 否 | 出发地,按行政区划名称填写,包括县(市、区)名称,可在县(市、区)后自主增添具体地址 | | `passenger_transport.destination_palce` | 否 | 到达地 | | `passenger_transport.vehicle_type` | 否 | 交通工具类型: | | `passenger_transport.level` | 否 | 等级,若交通工具为火车、飞机、船舶,则该要素为必填。等级类型: | 完整请求报文(公共字段已合并,改值即可直接调): ```json { "order_id": "ORD20260829001", "invoice_type_code": "082", "seller_taxpayer_num": "91110119MABQDU249D", "seller_name": "北京湛露科技有限公司", "seller_address": "北京市", "seller_tel": "01000000000", "seller_bank_name": "测试银行", "seller_bank_account": "1234567890", "title_type": 1, "buyer_title": "个人", "terminal_code": "91110119MABQDU249D", "drawer": "开灵测试", "callback_url": "https://你们的域名/invoice/callback", "template": 1, "amount_has_tax": "109.00", "amount_without_tax": "100.00", "tax_amount": "9.00", "taxpayer_confirmation": { "draw_with_nobuyer_flag": 1 }, "items": [ { "name": "*运输服务*客运服务费", "tax_code": "3010101010100000000", "unit": "次", "total": "1", "price": "100.00", "total_price": "100.00", "tax_rate": "0.09", "tax_amount": "9.00", "amount_has_tax": "109.00" } ], "specific_business_code": "09", "passenger_transport": [ { "traveler": "张三", "id_type": "201", "id_number": "110101199001011234", "travel_date": "2026-08-29", "departure_place": "北京市朝阳区", "destination_palce": "上海市浦东新区", "vehicle_type": "2", "level": "二等座" } ] } ``` #### 拖拉机/联合收割机 > 什么时候传:销售拖拉机、联合收割机时 > > **实测**:— 未实测:需农机销售资质 > > **触发条件**:业务代码 specific_business_code = 13(拖拉机和联合收割机) > > **加在哪里**:请求体顶层,与 items 平级(不是塞在 items 里) | 字段 | 必填 | 说明 | | --- | --- | --- | | `tractor_combine` | 否 | **数电拖拉机和联合收割机服务必填** | | `tractor_combine.chassis_number` | 否 | 底盘号或机架号 | | `tractor_combine.engine_number` | 是 | 发动机号 | | `tractor_combine.is_register` | 否 | 是否用于拖拉机和联合收割机登记 | 完整请求报文(公共字段已合并,改值即可直接调): ```json { "order_id": "ORD20260829008", "invoice_type_code": "082", "specific_business_code": "13", "seller_taxpayer_num": "91110119MABQDU249D", "seller_name": "北京湛露科技有限公司", "seller_address": "北京市", "seller_tel": "01000000000", "seller_bank_name": "测试银行", "seller_bank_account": "1234567890", "title_type": 2, "buyer_title": "深圳市示例科技有限公司", "buyer_taxpayer_num": "91440300MA5XXXXXXX", "terminal_code": "91110119MABQDU249D", "drawer": "张三", "callback_url": "https://你们的域名/invoice/callback", "template": 1, "amount_has_tax": "113.00", "amount_without_tax": "100.00", "tax_amount": "13.00", "taxpayer_confirmation": { "draw_with_nobuyer_flag": 1 }, "items": [ { "name": "*农机*轮式拖拉机", "tax_code": "1030201000000000000", "unit": "台", "total": "1", "price": "100.00", "total_price": "100.00", "tax_rate": "0.13", "tax_amount": "13.00", "amount_has_tax": "113.00" } ], "tractor_combine": { "machine_type": "拖拉机", "brand_model": "示例牌 TL904", "factory_number": "TL2026000001", "engine_number": "EN2026000001" }, "pay_info": [ { "pay_num": "PAY20260829001", "pay_channel_code": "001" } ] } ``` #### 二手车服务 > 什么时候传:做二手车拍卖、经纪等服务,需要关联原车辆销售发票时 > > **实测**:— 未实测:需关联真实的原二手车销售发票号码 > > **触发条件**:业务代码 specific_business_code = 15(数电二手车) > > **加在哪里**:请求体顶层,与 items 平级(不是塞在 items 里) | 字段 | 必填 | 说明 | | --- | --- | --- | | `used_car_service` | 否 | **数电二手车必填** | | `used_car_service.ticket_sn` | 是 | 数电发票号码 | | `used_car_service.paper_ticket_code` | 是 | 纸票发票代码 | | `used_car_service.paper_ticket_sn` | 是 | 纸票发票号码 | 完整请求报文(公共字段已合并,改值即可直接调): ```json { "order_id": "ORD20260829007", "invoice_type_code": "082", "specific_business_code": "15", "seller_taxpayer_num": "91110119MABQDU249D", "seller_name": "北京湛露科技有限公司", "seller_address": "北京市", "seller_tel": "01000000000", "seller_bank_name": "测试银行", "seller_bank_account": "1234567890", "title_type": 2, "buyer_title": "深圳市示例科技有限公司", "buyer_taxpayer_num": "91440300MA5XXXXXXX", "terminal_code": "91110119MABQDU249D", "drawer": "张三", "callback_url": "https://你们的域名/invoice/callback", "template": 1, "amount_has_tax": "106.00", "amount_without_tax": "100.00", "tax_amount": "6.00", "taxpayer_confirmation": { "draw_with_nobuyer_flag": 1 }, "items": [ { "name": "*经纪代理服务*二手车服务费", "tax_code": "3040601000000000000", "unit": "次", "total": "1", "price": "100.00", "total_price": "100.00", "tax_rate": "0.06", "tax_amount": "6.00", "amount_has_tax": "106.00" } ], "used_car_service": { "origin_invoice_code": "", "origin_invoice_number": "26117200000004866553", "origin_invoice_date": "2026-08-29" }, "pay_info": [ { "pay_num": "PAY20260829001", "pay_channel_code": "001" } ] } ``` #### 二手车销售 > 什么时候传:开二手车销售统一发票时 > > **实测**:— 未实测:二手车销售统一发票需专门票种权限 > > **触发条件**:票种 invoice_type_code 取二手车销售统一发票;业务代码 51 正常开具 / 52 反向开具 > > **加在哪里**:请求体顶层,与 items 平级(不是塞在 items 里) | 字段 | 必填 | 说明 | | --- | --- | --- | | `used_car_sell` | 否 | **二手车销售统一发票必填** | | `used_car_sell.frame_number` | 是 | 车辆识别代号/车架号码 | | `used_car_sell.license_plate_number` | 是 | 车牌照号 | | `used_car_sell.brand_model` | 是 | 厂牌型号 | | `used_car_sell.registration_certificate` | 是 | 登记证号 | | `used_car_sell.transfer_office_name` | 是 | 转入地车辆管理所名称 | | `used_car_sell.nature_of_enterprise` | 是 | 二手车企业性质 | 完整请求报文(公共字段已合并,改值即可直接调): ```json { "order_id": "ORD20260829006", "invoice_type_code": "082", "specific_business_code": "51", "seller_taxpayer_num": "91110119MABQDU249D", "seller_name": "北京湛露科技有限公司", "seller_address": "北京市", "seller_tel": "01000000000", "seller_bank_name": "测试银行", "seller_bank_account": "1234567890", "title_type": 2, "buyer_title": "深圳市示例科技有限公司", "buyer_taxpayer_num": "91440300MA5XXXXXXX", "terminal_code": "91110119MABQDU249D", "drawer": "张三", "callback_url": "https://你们的域名/invoice/callback", "template": 1, "amount_has_tax": "100.00", "amount_without_tax": "100.00", "tax_amount": "0.00", "taxpayer_confirmation": { "draw_with_nobuyer_flag": 1 }, "items": [ { "name": "*车辆*二手轿车", "tax_code": "1090501000000000000", "unit": "辆", "total": "1", "price": "100.00", "total_price": "100.00", "tax_rate": "0.00", "tax_amount": "0.00", "amount_has_tax": "100.00" } ], "used_car_sell": { "car_number": "京A12345", "vehicle_type": "轿车", "brand_model": "示例牌 SL7160", "vin": "LSGXXXXXXXXXXXXXX", "license_issuer": "北京市公安局公安交通管理局", "registration_certificate_no": "1101000000000", "transfer_vehicle_office": "北京市车管所" }, "pay_info": [ { "pay_num": "PAY20260829001", "pay_channel_code": "001" } ] } ``` **二、按场景补充(和业务代码无关)** —— 这几块不看业务代码,看的是主体性质、票种或模板;不要照着 `specific_business_code` 找它们。 #### 差额征税扣除 > 什么时候传:做差额征税,票面要体现扣除额时 > > **实测**:✅ 已实测开出发票(票号 26117200000004866588) > > **触发条件**:★不由业务代码触发。由本块自己的 tax_diff_type_code 决定:01 全额开票 / 02 差额开票 > > **加在哪里**:请求体顶层,与 items 平级(不是塞在 items 里) | 字段 | 必填 | 说明 | | --- | --- | --- | | `deduction_service` | 否 | **差额征税差额开票** 必填 (发票版式只有一行明细) | | `deduction_service.tax_diff_type_code` | 是 | 差额征税时必填 | | `deduction_service.deduction` | 是 | 扣除额 (明细汇总的扣除额 ,一个差额凭证明细的时候:扣除额 = 本次扣除金额),不大于凭证合计金额 ;如果是全额开票的话,备注不想要显示,扣除额为0 | | `deduction_service.item.` | 是 | 差额凭证明细集合 【全额开票备注不显示扣除额的话,可以不用传明细集内容,同时扣除额默认为0】 | | `deduction_service.item.deduction_amount` | 是 | 本次扣除金额 (差额凭证的每个明细扣除额,金额必须大于0 ) | | `deduction_service.item.remark` | 否 | 差额凭证明细备注(其他的发票或者凭证类型的时候必填) | | `deduction_service.item.paper_invoice_code` | 否 | 纸质发票代码 (凭证是增值税专用发票或者增值税普通发票类型必填) | | `deduction_service.item.digital_invoice_number` | 否 | 数电票号码 (凭证是数电类型必填) | | `deduction_service.item.paper_invoice_number` | 否 | 纸质发票号码 (凭证是增值税专用发票或者增值税普通发票类型必填) | | `deduction_service.item.total_amount_with_tax` | 是 | 含税销售额 (发票票面总金额) | | `deduction_service.item.issuance_date` | 是 | 开具日期,格式:yyyy-MM-dd | | `deduction_service.item.voucher_total_amount` | 是 | 凭证合计金额 (各明细的凭证金额之和,合计金额不能大于含税销售额,同时也不小于扣除额) | | `deduction_service.item.voucher_number` | 否 | 凭证号码 | | `deduction_service.item.voucher_type` | 是 | 凭证类型:01-数电票 02-增值税专用发票 03-增值税普通发票 04-营业税发票 05-财政票据 06-法院裁决书 07-契税完税凭证 08-其他发票类 09 其他扣除凭证 注意:一般建议选择其他扣除凭证,数电票需要传数电号码 | 完整请求报文(公共字段已合并,改值即可直接调): ```json { "order_id": "ORD20260829001", "invoice_type_code": "082", "seller_taxpayer_num": "91110119MABQDU249D", "seller_name": "北京湛露科技有限公司", "seller_address": "北京市", "seller_tel": "01000000000", "seller_bank_name": "测试银行", "seller_bank_account": "1234567890", "title_type": 1, "buyer_title": "个人", "terminal_code": "91110119MABQDU249D", "drawer": "开灵测试", "callback_url": "https://你们的域名/invoice/callback", "template": 1, "amount_has_tax": "106.00", "amount_without_tax": "100.00", "tax_amount": "6.00", "taxpayer_confirmation": { "draw_with_nobuyer_flag": 1 }, "items": [ { "name": "*旅游服务*旅游费", "tax_code": "3070301000000000000", "unit": "次", "total": "1", "price": "100.00", "total_price": "100.00", "tax_rate": "0.06", "tax_amount": "6.00", "amount_has_tax": "106.00" } ], "deduction_service": { "tax_diff_type_code": "02", "deduction_amount": "40.00" } } ``` #### 销方证件信息 > 什么时候传:★卖方是个人、不是公司的时候。比如个人把自己的房子租出去、个人到税局代开发票——这时销方栏填的是一个自然人,税局要求把这个人的证件带上 > > **实测**:— 未实测:需销方为自然人的主体 > > **触发条件**:★不由业务代码触发。看销方是谁:销方是自然人(个人)就必填,销方是企业则完全不用管这一块 > > **加在哪里**:请求体顶层,与 items 平级(不是塞在 items 里) | 字段 | 必填 | 说明 | | --- | --- | --- | | `seller_cert_info` | 否 | 销方证件信息。★只有销方是个人时才传;销方是企业不传这一块 | | `seller_cert_info.cert_type` | 否 | 销方证件类型。证件类型代码:201 居民身份证(最常用)/102 营业执照/208 外国护照/227 中国护照/210 港澳居民来往内地通行证/213 台湾居民来往大陆通行证/299 其他个人证件。另有 202 军官证、203 武警警官证、204 士兵证、205 军队离退休干部证、206 残疾人证、207 残疾军人证、209 港澳同胞回乡证、211 台胞证、212 往来港澳通行证、214 往来台湾通行证、215 外国人居留证 | | `seller_cert_info.cert_num` | 否 | 销方证件号码,如身份证号 | 完整请求报文(公共字段已合并,改值即可直接调): ```json { "order_id": "ORD20260829010", "invoice_type_code": "082", "seller_taxpayer_num": "91110119MABQDU249D", "seller_name": "北京湛露科技有限公司", "seller_address": "北京市", "seller_tel": "01000000000", "seller_bank_name": "测试银行", "seller_bank_account": "1234567890", "title_type": 2, "buyer_title": "深圳市示例科技有限公司", "buyer_taxpayer_num": "91440300MA5XXXXXXX", "terminal_code": "91110119MABQDU249D", "drawer": "张三", "callback_url": "https://你们的域名/invoice/callback", "template": 1, "amount_has_tax": "109.00", "amount_without_tax": "100.00", "tax_amount": "9.00", "taxpayer_confirmation": { "draw_with_nobuyer_flag": 1 }, "items": [ { "name": "*运输服务*运输服务费", "tax_code": "3010101010100000000", "unit": "次", "total": "1", "price": "100.00", "total_price": "100.00", "tax_rate": "0.09", "tax_amount": "9.00", "amount_has_tax": "109.00" } ], "seller_cert_info": { "cert_type": "201", "cert_number": "110101199001011234", "cert_name": "李四" }, "pay_info": [ { "pay_num": "PAY20260829001", "pay_channel_code": "001" } ] } ``` #### 购方证件信息 > 什么时候传:★开票给个人、且税局要求留存这个人身份的时候。注意不是所有开给个人的票都要——日常消费开「个人」抬头不用填;二手车、不动产这类税局要认购买人身份的场景才要 > > **实测**:✅ 已实测开出发票(票号 26117200000004866589) > > **触发条件**:★不由业务代码触发。看购方是谁:开票给自然人且属于需要留存证件的场景时填;开给企业不用管 > > **加在哪里**:请求体顶层,与 items 平级(不是塞在 items 里) | 字段 | 必填 | 说明 | | --- | --- | --- | | `buyer_cert_info` | 否 | 购方证件信息。★开票给个人且属于需要留存证件的特殊场景才传,普通个人抬头消费票不用传 | | `buyer_cert_info.cert_type` | 否 | 购方证件类型。证件类型代码:201 居民身份证(最常用)/102 营业执照/208 外国护照/227 中国护照/210 港澳居民来往内地通行证/213 台湾居民来往大陆通行证/299 其他个人证件。另有 202 军官证、203 武警警官证、204 士兵证、205 军队离退休干部证、206 残疾人证、207 残疾军人证、209 港澳同胞回乡证、211 台胞证、212 往来港澳通行证、214 往来台湾通行证、215 外国人居留证 | | `buyer_cert_info.cert_num` | 否 | 购方证件号码,如身份证号 | 完整请求报文(公共字段已合并,改值即可直接调): ```json { "order_id": "ORD20260829001", "invoice_type_code": "082", "seller_taxpayer_num": "91110119MABQDU249D", "seller_name": "北京湛露科技有限公司", "seller_address": "北京市", "seller_tel": "01000000000", "seller_bank_name": "测试银行", "seller_bank_account": "1234567890", "title_type": 1, "buyer_title": "王五", "terminal_code": "91110119MABQDU249D", "drawer": "开灵测试", "callback_url": "https://你们的域名/invoice/callback", "template": 1, "amount_has_tax": "1.00", "amount_without_tax": "0.88", "tax_amount": "0.12", "taxpayer_confirmation": { "draw_with_nobuyer_flag": 1 }, "items": [ { "name": "办公用品", "tax_code": "1080118000000000000", "unit": "个", "total": "1", "price": "0.88", "total_price": "0.88", "tax_rate": "0.13", "tax_amount": "0.12", "amount_has_tax": "1.00" } ], "buyer_cert_info": { "cert_type": "201", "cert_num": "110101199001011234" } } ``` #### 冠名发票 > 什么时候传:云南区块链商户开冠名发票时 > > **实测**:— 未实测:需云南区块链商户与冠名模板权限 > > **触发条件**:★不由业务代码触发。由 template 决定:非冠名传 1;冠名传 2 丽江 / 3 石林 / 4 新版自定义。同场景 store_no 门店编码也必填 > > **加在哪里**:请求体顶层,与 items 平级(不是塞在 items 里) | 字段 | 必填 | 说明 | | --- | --- | --- | | `info` | 否 | **云南区块链商户使用冠名票发票模板时必填** | | `info.use_date` | 否 | 入园日期,使用冠名票发票模板时必填 | | `info.ticket_name` | 否 | 票据名称,使用冠名模板时必填 | 完整请求报文(公共字段已合并,改值即可直接调): ```json { "order_id": "ORD20260829012", "invoice_type_code": "082", "seller_taxpayer_num": "91110119MABQDU249D", "seller_name": "北京湛露科技有限公司", "seller_address": "北京市", "seller_tel": "01000000000", "seller_bank_name": "测试银行", "seller_bank_account": "1234567890", "title_type": 2, "buyer_title": "深圳市示例科技有限公司", "buyer_taxpayer_num": "91440300MA5XXXXXXX", "terminal_code": "91110119MABQDU249D", "drawer": "张三", "callback_url": "https://你们的域名/invoice/callback", "template": 3, "amount_has_tax": "106.00", "amount_without_tax": "100.00", "tax_amount": "6.00", "taxpayer_confirmation": { "draw_with_nobuyer_flag": 1 }, "items": [ { "name": "*旅游服务*景区门票", "tax_code": "3070301000000000000", "unit": "张", "total": "1", "price": "100.00", "total_price": "100.00", "tax_rate": "0.06", "tax_amount": "6.00", "amount_has_tax": "106.00" } ], "info": { "use_date": "20260901", "ticket_name": "石林风景区门票" }, "pay_info": [ { "pay_num": "PAY20260829001", "pay_channel_code": "001" } ], "store_no": "FFX001" } ``` #### 纳税人确认 > 什么时候传:★数电票的三个开关。最常用的是第二个:开票给个人、没有购方税号时,要传 1 才让开——开灵实测跑通的报文就带着它。另两个按需:做再生资源回收的、放弃 1% 征收率的 > > **实测**:✅ 已实测开出发票(票号 26117200000004866590) > > **触发条件**:★不由业务代码触发。票种为数电票(081/082 等)即适用 > > **加在哪里**:请求体顶层,与 items 平级(不是塞在 items 里) | 字段 | 必填 | 说明 | | --- | --- | --- | | `taxpayer_confirmation` | 否 | 纳税人确认信息,只有全电发票(数电票)才用。三个子字段都可不传,但下面第二个实际上几乎都要传 | | `taxpayer_confirmation.draw_with_nobuyer_flag` | 否 | ★试点阶段无购方纳税人信息继续开票标志:1 是 / 0 否。★开给个人、没有购方税号时传 1,否则可能开不出来——开灵实测跑通的报文就传的 1 | | `taxpayer_confirmation.renewable_resource_recycling_flag` | 否 | 再生资源回收单位标志:1 是 / 0 否。做再生资源回收的传 1,其余不传 | | `taxpayer_confirmation.not_dutyfree_reason` | 否 | 放弃享受减按 1% 征收率的原因,默认传空。04 前期已开票需红冲或重开;05 因实际经营需要放弃该政策 | 完整请求报文(公共字段已合并,改值即可直接调): ```json { "order_id": "ORD20260829001", "invoice_type_code": "082", "seller_taxpayer_num": "91110119MABQDU249D", "seller_name": "北京湛露科技有限公司", "seller_address": "北京市", "seller_tel": "01000000000", "seller_bank_name": "测试银行", "seller_bank_account": "1234567890", "title_type": 1, "buyer_title": "个人", "terminal_code": "91110119MABQDU249D", "drawer": "开灵测试", "callback_url": "https://你们的域名/invoice/callback", "template": 1, "amount_has_tax": "1.00", "amount_without_tax": "0.88", "tax_amount": "0.12", "taxpayer_confirmation": { "draw_with_nobuyer_flag": 1, "renewable_resource_recycling_flag": 0 }, "items": [ { "name": "办公用品", "tax_code": "1080118000000000000", "unit": "个", "total": "1", "price": "0.88", "total_price": "0.88", "tax_rate": "0.13", "tax_amount": "0.12", "amount_has_tax": "1.00" } ] } ``` **返回参数(data 内)** | 字段 | 说明 | | --- | --- | | `state` | 受理状态,1 表示已受理、开票中 | | `order_sn` | 通道受理号。★查结果时可以用它,也可以用你自己的 order_id | | `invoice_id` | 同 order_sn(值一样) | | `mock` | true=演示数据,未真实开票 | **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "data": { "state": 1, "order_sn": "7499487274632001743", "invoice_id": "7499487274632001743", "mock": false } } ``` **返回示例(失败)** ```json { "code": "CYT_50200", "data": { "upstreamCode": 1014, "upstreamMessage": "第1项商品明细中,开票明细金额校验失败" } } ``` **异常与处理** | 情形 | 含义与处理 | | --- | --- | | 开票明细金额校验失败 | ★最常见。要求 price × total 精确等于 total_price,且 total_price + tax_amount = amount_has_tax。用能整除的数字,别让小数除不尽 | | 无法映射到「成品油单位」枚举 | ★成品油的 unit 只能填 LITER 或 TON(英文大写)。填中文「升」「吨」会报这个错 | | 企业开具成品油发票必须有成品油经销、生产资质 | 销方企业没有成品油经营资质。这是税务资质问题,不是接口问题,需企业向主管税务机关办理 | | tax_code 不对 | 税收分类编码要按油品/商品准确填,汽油为 1070101010100000000 | | order_id 重复 | 同一个订单号不能重复开票 | **前端怎么用** ```js const r = await post('/api/v1/invoice/forward/create', payload); save(r.data.order_sn); // 存下来,查结果要用 // 开票是异步的,接着轮询②查结果 ``` > 所有业务共用这一个开票接口,区别只在于「除了基础字段,还要不要多传一块特定业务字段」。选你的场景,照那一块的完整报文改值即可。 --- ### ② 查开票结果 拿状态、发票号码和票文件。★票的 PDF / OFD / XML 三种格式都在这个接口返回,不需要再调别的接口下载。 `POST https://openapi.kailingteck.com/api/v1/invoice/forward/query` **请求参数** | 字段 | 必填 | 说明 | | --- | --- | --- | | `seller_taxpayer_num` | 必填 | 销方税号(统一社会信用代码,15/17/18/20 位,不含字母 I O S V Z)。★字段名就是 seller_taxpayer_num,写成 taxpayer_id、tax_no 都不行——写错时上游回的是「税号不能为空」,明明传了却说没传,极易误判 | | `order_id` | 二选一 | 你侧订单号(①开票时传的那个)。与 order_sn 不能同时为空 | | `order_sn` | 二选一 | ①返回的通道受理号。★注意不能只传 invoice_id | | `is_red` | 选填 | 查哪种票:0 蓝票(默认)/ 1 红票。★查红票必须传 1,否则查不到。也可以直接用 ④ 查红票接口,那个已经帮你固定成 1 了 | **请求示例** ```json { "order_id": "ORD20260829001", "seller_taxpayer_num": "91110119MABQDU249D" } ``` **按业务分块的完整参数** #### 开票结果回调(不轮询就看这个) > 什么时候传:①开票时传了 callback_url,开票状态一变就 POST 推给你 > > **触发条件**:★与④查红票用的是同一个回调接口、同一个地址。本次推的是蓝票,notify_type = invoice.blue > > **加在哪里**:你自己的服务端接收,POST + Content-Type: application/json;处理完返回 code 为 0,否则平台会重推(最多 10 次,间隔 1m/10m/10m/30m/30m/60m/60m/120m/180m/360m) 返回参数: | 字段 | 说明 | | --- | --- | | `appkey` | 你的 app_key,用于识别是哪家的回调 | | `notify_type` | ★通知类型:invoice.blue 蓝票回调 / invoice.red 红票回调 | | `notify_time` | 通知时间,格式 2026-08-29 10:20:12 | | `message` | 结果详情 | | `order_id` | 你侧订单号 | | `order_sn` | 通道受理号 | | `g_unique_id` | 同 order_sn,老平台兼容字段。新接入的用 order_sn 就行 | | `ticket_status` | ★发票状态:1 开票中 / 2 开票失败 / 3 开票成功 / 4 作废中 / 5 作废失败 / 6 作废成功 | | `ticket_sn` | 发票号码。开票失败时为空 | | `ticket_code` | 发票代码。开票失败时为空;数电票本来就没有 | | `ticket_date` | 开票时间 | | `ticket_total_amount_has_tax` | 发票含税总价。失败时为空 | | `ticket_total_amount_no_tax` | 发票不含税总价。失败时为空 | | `ticket_tax_amount` | 发票税额。失败时为空 | | `check_code` | 发票校验码。数电票无校验码,返回空 | | `cipher_text` | 发票密文。数电票无密文,返回空 | | `qrcode` | 发票二维码 base64 内容,最大 10KB。渠道不同可能为空 | | `pdf_url` | 发票 PDF 地址(已转存为开灵地址) | | `ofd_url` | 发票 OFD 地址(数电票才有) | | `xml_url` | 发票 XML 地址(数电票才有) | | `tax_bureau_url` | 税局端下载页地址 | | `fail_code` | 开票失败且需要人工处理时才有值:1000 需要实人认证 / 1001 需要重新登录 | **返回参数(data 内)** | 字段 | 说明 | | --- | --- | | `order_id` | 你侧订单号。与 order_sn 不会同时为空 | | `order_sn` | 通道受理号(上游侧的订单唯一标识) | | `status` | ★开票状态:0 无效 / 1 开票中 / 2 开票失败 / 3 开票成功 / 4 作废中 / 5 作废失败 / 6 作废成功。★数电票没有作废,4~6 见不到 | | `message` | 开票描述。失败时这里是原因 | | `ticket_date` | 开票日期 | | `ticket_sn` | ★发票号码(开成功才有) | | `ticket_code` | 发票代码。★数电票没有发票代码,这里返回空或 0,属正常 | | `check_code` | 校验码。★数电票没有校验码,返回空,属正常 | | `amount_with_tax` | 含税金额(元) | | `amount_without_tax` | 不含税金额(元) | | `tax_amount` | 税额(元) | | `is_red_washed` | 是否已被红冲:0 未红冲 / 1 已红冲 | | `pdf_url` | ★发票 PDF 下载地址(开灵自有地址,24 小时有效) | | `ofd_url` | 发票 OFD 下载地址(数电票才有) | | `xml_url` | 发票 XML 下载地址(数电票才有) | | `tax_bureau_url` | 税局端下载页地址,该页面含 PDF / OFD / XML 三个下载入口 | | `buyer_info` | 购方信息回显 | **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "data": { "order_id": "", "order_sn": "7499487274632001743", "status": 3, "message": "开票成功", "ticket_date": "2026-08-29", "ticket_sn": "26117200000004866581", "ticket_code": "0", "check_code": "", "amount_with_tax": "1", "amount_without_tax": "0.88", "tax_amount": "0.12", "is_red_washed": 0, "pdf_url": "https://openapi.kailingteck.com/api/v1/invoice/forward/file/51f659315f9a0f7a648f571087144cfa__26117200000004866581.pdf", "ofd_url": "https://openapi.kailingteck.com/api/v1/invoice/forward/file/c91c46f3045b6ba3e80f127a963327a5__26117200000004866581.ofd", "xml_url": "https://openapi.kailingteck.com/api/v1/invoice/forward/file/4ea8bb05896f1c1e982de6c07b1e0ea1__26117200000004866581.xml", "tax_bureau_url": "", "buyer_info": { "title_type": "1", "buyer_title": "个人", "buyer_taxpayer_num": "", "buyer_address": "", "buyer_phone": "", "buyer_bank_name": "", "buyer_bank_account": "", "buyer_email": "" }, "mock": false } } ``` **返回示例(失败)** ```json { "code": "CYT_00000", "data": { "status": 2, "message": "蓝票开票失败:具体原因见此字段", "ticket_sn": "0", "pdf_url": "" } } ``` **异常与处理** | 情形 | 含义与处理 | | --- | --- | | 税号不能为空(但你明明传了) | ★销方税号的字段名写错了。必须叫 seller_taxpayer_num,不是 taxpayer_id 也不是 tax_no。上游收不到这个字段就报「为空」,不会告诉你是名字错了 | | appkey未被 xxx 授权 | ★销方企业还没把开票能力授权给开灵平台的应用。不是参数问题、也不是接入方能自行解决的,请联系开灵,由销方企业在乐企平台完成授权 | | 找不到对应的发票信息 | 订单号不对,或①开票根本没受理成功 | | status 一直是 1 | 还在开票中。建议间隔 3~5 秒轮询,设最大次数,别写死循环 | | status=2 | 开票失败,读 message 拿原因。常见:金额校验、税收编码、税盘未绑定 | | 票文件链接打不开 | 链接 24 小时有效,过期重新调本接口即可拿到新链接 | **前端怎么用** ```js // 轮询直到 status === 3 const r = await post('/api/v1/invoice/forward/query', { order_id, seller_taxpayer_num }); if (r.data.status === 3) download(r.data.pdf_url); ``` > 不想轮询的话,在①开票时配好 callback_url,开票结果会主动推给你——★这个回调和④查红票是同一个接口、同一个地址,蓝票红票都走它,用 notify_type 区分。票文件已由开灵转存,给到的是开灵自有下载地址,不暴露上游。 --- ### ③ 红冲 开错了、退货了,用它把原票冲掉。★这是一键接口——申请红字信息表、下载信息表、开红票三步已经合成在里面,你只调这一个就完事。★数电票开出即入库,没有「作废」这回事,只能红冲。 `POST https://openapi.kailingteck.com/api/v1/invoice/forward/red` **请求参数** | 字段 | 必填 | 说明 | | --- | --- | --- | | `invoices[]` | 必填 | 红冲发票信息列表,支持一次冲多张 | | `invoices[].seller_taxpayer_num` | 必填 | 销方税号 | | `invoices[].order_id` | 二选一 | 原蓝票的订单号(你开票时传的) | | `invoices[].order_sn` | 二选一 | 原蓝票的通道受理号。★两个都传时以 order_sn 为准 | | `invoices[].apply_reason` | 必填 | ★申请原因,是代码不是文字:2 开票有误(购方拒收/尚未交付)/3 销货退回/4 服务中止/5 销售折让。传空默认 2。⚠ 实测写成「开票有误」这种自由文本会被拒:「全电发票红冲原因不合法」。全电发票只支持销方申请 | | `invoices[].callback_url` | 必填 | 红冲结果回传地址,推送格式同②的开票结果回调(notify_type 为 invoice.red) | | `invoices[].red_serial_no` | 选填 | 红字信息表编码。★走一键红冲用不到;只有纸质专票红冲才需要自己先申请信息表再填这里 | | `invoices[].confirm_info_uuid` | 选填 | 红字确认单 uuid。★同上,一键红冲不用填 | | `invoices[].confirm_info_state` | 选填 | 红字确认信息状态代码,如 01。★一键红冲不用填 | | `invoices[].special_element_code` | 选填 | 特定要素代码,如 08。★一键红冲不用填 | | `invoices[].tax_to_ele_channel` | 选填 | 税控发票走数电通道红冲时填 1,默认 0。★一键红冲不用填 | **请求示例** ```json { "invoices": [ { "seller_taxpayer_num": "91110119MABQDU249D", "order_id": "REG1788017096448", "apply_reason": "2", "callback_url": "https://openapi.kailingteck.com/api/callback/gaodeng" } ] } ``` **返回参数(data 内)** | 字段 | 说明 | | --- | --- | | `list` | ★返回是数组,放在 data.list 下。一次冲多张时逐条对应,顺序与请求里的 invoices 一致 | | `list[].state` | ★红冲状态:1 已提交成功(红票还没到手,要再查)/ 3 提交失败或已红冲过 | | `list[].message` | 提交红冲的说明。state=3 时这里是原因 | | `list[].order_sn` | 原蓝票的通道受理号 | | `list[].invoice_id` | 红票的唯一标识 | **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "data": { "list": [ { "state": 1, "message": "success", "order_sn": "7499487274632001743", "invoice_id": "7499487521907153138" } ], "mock": false } } ``` **异常与处理** | 情形 | 含义与处理 | | --- | --- | | 原票不存在或未开成功 | 只有开票成功(status=3)的票才能红冲 | | 已红冲过 | 查②看 is_red_washed 是否已为 1,不要重复冲 | | state=3「全电发票红冲原因不合法」 | ★apply_reason 传了自由文本。必须传代码 2/3/4/5,传空默认 2 | | 调完红冲但没拿到红票 | ★正常。本接口只负责提交,红票要用 /api/v1/invoice/forward/red-query 再查一次,或等 callback_url 推送——和开票 ①→② 是一个道理 | | 红票迟迟查不到,以为失败了 | ★红冲比开票慢得多。开灵实测:开票约 6 秒出票,红冲约 48 秒才出红票号。查红冲请按 8 秒一次、留 6~8 次余量;照开票的 3~5 秒节奏去等,会误判成失败进而重复提交红冲 | **前端怎么用** ```js const r = await post('/api/v1/invoice/forward/red', { invoices: [{ seller_taxpayer_num, order_id, apply_reason: '2', callback_url }] }); if (r.data.list[0].state !== 1) throw new Error(r.data.list[0].message); // 一键完成申请信息表→下载→红冲三步;结果走回调,或用 red-query 查 ``` > ★联调脚本:仓库 scripts/e2e-forward.js 跑一遍 ①→②→③→④ 全链路(APP_KEY=xxx APP_SECRET=xxx node scripts/e2e-forward.js)。★这是一键接口:上游把「申请红字信息表 + 自动下载信息表 + 红冲」三个动作合成了,你不用自己走三步。但★红冲和开票是同一个套路——本接口只是「提交」,返回 state=1 表示已受理,红票本身还没到手,要么等回调推送、要么用 /api/v1/invoice/forward/red-query 再查一次。红冲成功后原蓝票的 is_red_washed 会变成 1。 --- ### ④ 查红票 红冲提交成功不等于红票到手。用这个接口把红票的号码和 PDF 拿回来——和开票 ①→② 是完全一样的套路。 `POST https://openapi.kailingteck.com/api/v1/invoice/forward/red-query` **请求参数** | 字段 | 必填 | 说明 | | --- | --- | --- | | `seller_taxpayer_num` | 必填 | 销方税号 | | `order_id` | 二选一 | ★原蓝票的订单号(不是红票的)。红票挂在原蓝票的订单下 | | `order_sn` | 二选一 | 原蓝票的通道受理号 | **请求示例** ```json { "order_id": "ORD20260829001", "seller_taxpayer_num": "91110119MABQDU249D" } ``` **按业务分块的完整参数** #### 开票结果回调(不轮询就看这个) > 什么时候传:③红冲时传了 callback_url,红票开好就 POST 推给你 > > **触发条件**:★与②查开票结果用的是同一个回调接口、同一个地址。本次推的是红票,notify_type = invoice.red > > **加在哪里**:你自己的服务端接收,POST + Content-Type: application/json;处理完返回 code 为 0,否则平台会重推(最多 10 次,间隔 1m/10m/10m/30m/30m/60m/60m/120m/180m/360m) 返回参数: | 字段 | 说明 | | --- | --- | | `appkey` | 你的 app_key,用于识别是哪家的回调 | | `notify_type` | ★通知类型:invoice.blue 蓝票回调 / invoice.red 红票回调 | | `notify_time` | 通知时间,格式 2026-08-29 10:20:12 | | `message` | 结果详情 | | `order_id` | 你侧订单号 | | `order_sn` | 通道受理号 | | `g_unique_id` | 同 order_sn,老平台兼容字段。新接入的用 order_sn 就行 | | `ticket_status` | ★发票状态:1 开票中 / 2 开票失败 / 3 开票成功 / 4 作废中 / 5 作废失败 / 6 作废成功 | | `ticket_sn` | 发票号码。开票失败时为空 | | `ticket_code` | 发票代码。开票失败时为空;数电票本来就没有 | | `ticket_date` | 开票时间 | | `ticket_total_amount_has_tax` | 发票含税总价。失败时为空 | | `ticket_total_amount_no_tax` | 发票不含税总价。失败时为空 | | `ticket_tax_amount` | 发票税额。失败时为空 | | `check_code` | 发票校验码。数电票无校验码,返回空 | | `cipher_text` | 发票密文。数电票无密文,返回空 | | `qrcode` | 发票二维码 base64 内容,最大 10KB。渠道不同可能为空 | | `pdf_url` | 发票 PDF 地址(已转存为开灵地址) | | `ofd_url` | 发票 OFD 地址(数电票才有) | | `xml_url` | 发票 XML 地址(数电票才有) | | `tax_bureau_url` | 税局端下载页地址 | | `fail_code` | 开票失败且需要人工处理时才有值:1000 需要实人认证 / 1001 需要重新登录 | **返回参数(data 内)** | 字段 | 说明 | | --- | --- | | `(字段与②查开票结果完全一致)` | 只是查回来的是红票:金额和税额为负数 | | `status` | ★红票开具状态:1 开票中 / 2 开票失败 / 3 开票成功 | | `ticket_sn` | ★红票号码 | | `ticket_date` | 红票开票日期 | | `amount_with_tax` | 含税金额,★负数 | | `amount_without_tax` | 不含税金额,★负数 | | `tax_amount` | 税额,★负数 | | `pdf_url` | ★红票 PDF 下载地址(开灵自有地址,24 小时有效) | | `ofd_url` | 红票 OFD 下载地址 | | `xml_url` | 红票 XML 下载地址 | | `tax_bureau_url` | 税局端下载页地址 | **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "data": { "order_id": "", "order_sn": "7499487274632001743", "status": 3, "message": "开票成功", "ticket_date": "2026-08-29", "ticket_sn": "26117200000004866582", "ticket_code": "0", "check_code": "", "amount_with_tax": "-1", "amount_without_tax": "-0.88", "tax_amount": "-0.12", "is_red_washed": 1, "pdf_url": "https://openapi.kailingteck.com/api/v1/invoice/forward/file/0c3b402072d48ffd17689e6830ca6280__26117200000004866582.pdf", "ofd_url": "https://openapi.kailingteck.com/api/v1/invoice/forward/file/133b6ebc09eef0fb91aa45dbef731493__26117200000004866582.ofd", "xml_url": "", "tax_bureau_url": "", "buyer_info": { "title_type": "1", "buyer_title": "个人", "buyer_taxpayer_num": "", "buyer_address": "", "buyer_phone": "", "buyer_bank_name": "", "buyer_bank_account": "", "buyer_email": "" }, "mock": false } } ``` **异常与处理** | 情形 | 含义与处理 | | --- | --- | | CYT_10001 查无此票 | ★最常见,且不是故障:红冲还没走完。③返回 state=1 只是受理,红票要几秒到几分钟才出来,隔几秒再查即可,不要当成通道异常而中断轮询 | | 查回来的是蓝票 | 本接口已固定按红票查(内部把 is_red 置 1),如果你直接调②,记得自己传 is_red=1 | | status=2 | 红冲失败,读 message 拿原因 | **前端怎么用** ```js // ③提交红冲后,轮询本接口拿红票 const r = await post('/api/v1/invoice/forward/red-query', { order_id, seller_taxpayer_num }); if (r.data.status === 3) download(r.data.pdf_url); // 金额是负数 ``` > ★传的是原蓝票的订单号,不是红票的——红票挂在原蓝票订单下。不想轮询就用③里的 callback_url,红票开好会主动推给你——★这个回调和②查开票结果是同一个接口、同一个地址,蓝票推 notify_type=invoice.blue、红票推 invoice.red,你的服务端一个入口就够,不用做两套。本接口不计费。 --- ### ⑤ 发票统计与对账 月底对账用。一个接口,靠 URL 上的 type 参数决定查什么,共 5 种。★统计类都是「先发起、再查结果」的两步式:发起时给回调地址,拿到 record_id,再用对应的 -query 拿数据。 `POST https://openapi.kailingteck.com/api/v1/invoice/forward/statistic?type=detail` **请求参数** | 字段 | 必填 | 说明 | | --- | --- | --- | | `type(URL 参数)` | 必填 | ★决定查什么,5 选 1:detail 销项发票明细 / tax-rate 发起税率统计 / tax-rate-query 查税率统计结果 / declare 发起申报统计 / declare-query 查申报统计结果 | | `(其余参数按 type 不同而不同)` | — | ★请展开下面对应的业务块,看该 type 具体要传什么、返回什么 | **请求示例** ```json POST /api/v1/invoice/forward/statistic?type=detail { "tax_code": "91110119MABQDU249D", "invoice_type_code": "026", "tax_rate": "0.13", "year": 2026, "month": 8, "page": 0, "size": 10, "callback_url": "https://你们的域名/statistic-callback" } ``` **按业务分块的完整参数** #### type=detail|销项发票明细查询 > 什么时候传:要拉一个月开出去的发票流水明细时 | 字段 | 必填 | 说明 | | --- | --- | --- | | `tax_code` | 必填 | 企业税号 | | `tax_rate` | 必填 | 税率,取值:"0" "0.01" "0.03" "0.04" "0.05" "0.06" "0.09" "0.10" "0.11" "0.13" "0.16" "0.17" | | `year` | 必填 | 统计年份,整数,如 2026 | | `month` | 必填 | 统计月份,整数,如 8 | | `callback_url` | 必填 | 回调地址,明细结果异步推到这里 | | `page` | 必填 | 页码,★0 是第一页 | | `size` | 必填 | 每页发票数 | | `invoice_type_code` | 选填 | 票种,不填查所有:004 增值税专用发票 / 007 增值税普通发票 / 025 增值税卷式发票 / 026 增值税电子发票 / 028 增值税电子专票 / 032 区块链电子发票 | | `accounting_subject` | 选填 | 核算主体 | | `buyer_name` | 选填 | 购方名称,按购方筛 | 返回参数: | 字段 | 说明 | | --- | --- | | `record_id` | 任务 ID。★明细数据本身只走 callback_url 推送,没有配套的 -query 接口 | #### type=tax-rate|发起税率统计 > 什么时候传:按税率汇总一个月的开票金额、税额、份数时(第一步:发起) | 字段 | 必填 | 说明 | | --- | --- | --- | | `tax_code` | 必填 | 企业税号 | | `invoice_type_code` | 必填 | 票种:004 专票 / 007 普票 / 025 卷票 / 026 电子发票 / 028 电子专票 / 032 区块链电子发票 | | `year` | 必填 | 统计年份 | | `month` | 必填 | 统计月份 | | `callback_url` | 必填 | 回调地址,统计结果异步推到这里 | | `terminal_code` | 选填 | 税盘终端号/盘号/分机号 | | `accounting_subject` | 选填 | 核算主体 | | `buyer_name` | 选填 | 购方名称 | 返回参数: | 字段 | 说明 | | --- | --- | | `record_id` | ★统计结果 ID。拿它调 type=tax-rate-query 取数据 | #### type=tax-rate-query|查税率统计结果 > 什么时候传:拿到 record_id 后取税率统计数据(第二步:取结果) | 字段 | 必填 | 说明 | | --- | --- | --- | | `record_id` | 必填 | 上一步 tax-rate 返回的统计结果 ID | 返回参数: | 字段 | 说明 | | --- | --- | | `record_id` | 统计结果 ID | | `accounting_subject` | 核算实体 | | `buyer_name` | 购方名称 | | `record_list` | ★统计结果集,按税率一行一条 | | `record_list[].tax_rate` | 税率 | | `record_list[].blue_price` | 正数发票累计金额 | | `record_list[].blue_tax` | 正数发票累计税额 | | `record_list[].blue_price_tax` | 正数发票价税合计 | | `record_list[].invalid_blue_price` | 正废发票累计金额 | | `record_list[].invalid_blue_tax` | 正废发票累计税额 | | `record_list[].invalid_blue_price_tax` | 正废发票价税合计 | | `record_list[].red_price` | 负数发票累计金额 | | `record_list[].red_tax` | 负数发票累计税额 | | `record_list[].red_price_tax` | 负数发票价税合计 | | `record_list[].invalid_red_price` | 负废发票累计金额 | | `record_list[].invalid_red_tax` | 负废发票累计税额 | | `record_list[].invalid_red_price_tax` | 负废发票价税合计 | | `record_list[].sell_price` | ★实际销售金额(扣掉红冲和作废后的净额,对账看这个) | | `record_list[].sell_tax` | ★实际销售税额 | | `quantity_blue` | 蓝票份数 | | `quantity_invalid_blue` | 蓝票作废份数 | | `quantity_red` | 红票份数 | | `quantity_invalid_red` | 红票作废份数 | #### type=declare|发起申报统计 > 什么时候传:做增值税申报,要按免税/即征即退等口径出数时(第一步:发起) | 字段 | 必填 | 说明 | | --- | --- | --- | | `tax_code` | 必填 | 企业税号 | | `invoice_type_code` | 必填 | 票种:004 专票 / 007 普票 / 025 卷票 / 026 电子发票 / 028 电子专票 / 032 区块链电子发票 | | `declare_type` | 必填 | ★申报类型:1 免税 / 2 不征税 / 3 即征即退3% / 4 即征即退4% / 5 即征即退5% / 6 即征即退6% / 7 即征即退9%或10%或11% / 8 即征即退13% / 9 即征即退16%或17% / 10 非即征即退 | | `year` | 必填 | 统计年份 | | `month` | 必填 | 统计月份 | | `callback_url` | 必填 | 回调地址 | | `terminal_code` | 选填 | 税盘终端号/盘号/分机号 | | `accounting_subject` | 选填 | 核算主体 | 返回参数: | 字段 | 说明 | | --- | --- | | `record_id` | ★统计结果 ID。拿它调 type=declare-query 取数据 | #### type=declare-query|查申报统计结果 > 什么时候传:拿到 record_id 后取申报统计数据(第二步:取结果) | 字段 | 必填 | 说明 | | --- | --- | --- | | `record_id` | 必填 | 上一步 declare 返回的统计结果 ID | 返回参数: | 字段 | 说明 | | --- | --- | | `record_id` | 统计结果 ID | | `declare_type` | 申报类型代码,回显发起时传的值 | | `invoice_type_code` | 票种代码,回显发起时传的值 | | `record_map` | ★统计结果集。是个 map,键是归类名称,值是该类的汇总 | | `record_map.key` | ★归类名称:1 应税货物 / 2 应税劳务 / 3 服务、不动产和无形资产 / 4 货物及加工修理修配劳务 / 5 合计 | | `record_map.value.price` | 金额 | | `record_map.value.tax` | 税额 | | `record_map.value.total` | 价税合计 | | `record_map.value.quantity` | 发票份数 | **返回参数(data 内)** | 字段 | 说明 | | --- | --- | | `(返回随 type 不同)` | ★发起类(detail / tax-rate / declare)统一只返回一个 record_id;数据本身走回调,或用对应的 -query 接口拿 | | `record_id` | 统计任务 ID。拿着它去调 tax-rate-query 或 declare-query 取结果 | **返回示例(成功)** ```json { "code": "CYT_00000", "data": { "record_id": "5f082009b4ff7d6b03389fbf", "mock": false } } ``` **异常与处理** | 情形 | 含义与处理 | | --- | --- | | type 不合法 | 只能是 detail / tax-rate / tax-rate-query / declare / declare-query 五个之一 | | 统计结果为空 / record_id 查不到数据 | ★统计是异步的。发起后要等上游算完,隔几秒再用 -query 查,或者干脆等 callback_url 推送 | | 明细查不到第二页 | page 从 ★0 开始,不是 1 | | 申报统计数字对不上 | 确认 declare_type 传对了——免税和即征即退各档是分开统计的,不是一次出全 | **前端怎么用** ```js // 两步式:先发起,拿 record_id,再取结果 const t = await post('/api/v1/invoice/forward/statistic?type=tax-rate', { tax_code, invoice_type_code: '026', year: 2026, month: 8, callback_url, }); await sleep(3000); const r = await post('/api/v1/invoice/forward/statistic?type=tax-rate-query', { record_id: t.data.record_id, }); // 对账看 record_list[].sell_price / sell_tax(已扣红冲作废) ``` > ★本接口不计费,随便查。只有开票计费,一张票算一次。detail 明细只走回调,没有配套的 -query。⚠ 实测提示(2026-08-29 复测):本接口的 5 个 type 目前一律返回通道 503——发票统计服务在上游是独立开通的模块,开灵当前账号尚未开通;同账号同签名的开票、查票、红冲均正常。下面的请求与返回字段依据通道方接口文档整理,参数结构可直接照用,待开通后我们会补上实测报文。接入前请先找开灵确认贵司账号已开通统计服务。 --- ## 5. 写代码时的硬约束(AI 必读) 1. **开票是异步的**:①只返回受理号,票要轮询②拿。不要以为①返回成功票就开好了。 2. **不要只看 `code`**:`code=CYT_00000` 是调用成功,业务成功要看 `data.status===3`。 3. **金额必须精确**:`price × total` 要精确等于 `total_price`,且 `total_price + tax_amount = amount_has_tax`。用能整除的数字,除不尽会被税局挡回(报「开票明细金额校验失败」)。 4. **成品油不是另一个接口**:还是①,只是多传 `specific_business_code="01"`、`special_invoice_kind="08"`、`e_extra_info`。 5. **成品油的 `items[].unit` 只能填 `LITER`(升)或 `TON`(吨)**,英文大写。写中文「升」「吨」税局会报「无法映射到成品油单位枚举」。普通商品不受此限。 6. **开成品油票要求销方企业有成品油经销资质**,没有资质税局会拒,这不是接口问题。 7. **`e_extra_info` 是 JSON 字符串,不是对象**:要 `JSON.stringify` 之后再放进去。 8. **票文件不用另外下载**:②的返回里 `pdf_url`/`ofd_url`/`xml_url` 直接就是开灵自有下载地址,**24 小时有效**,过期重新调②即可。 9. **数电票没有作废,只有红冲**。不要去找作废接口。 10. **红冲是一键的**:③已经把「申请红字信息表 + 下载信息表 + 开红票」合成在一个接口里,**不要自己拆成三步调**。查红冲进度用 `/api/v1/invoice/forward/red-query`。 11. **红冲的入参是 `invoices` 数组**,支持一次冲多张;`order_id` 与 `order_sn` 二选一,都传时以 `order_sn` 为准。 12. **开票与红冲的轮询节奏差一个数量级**:开灵实测,开票约 **6 秒**出票,红冲约 **48 秒**才出红票号。查开票结果按 3~5 秒一次没问题;**查红冲结果要按 8 秒一次、留 6~8 次的余量**,照开票的节奏去等红冲,会误判成失败进而重复提交。两者都要设最大次数,不要写死循环。 13. **查询类接口的销方税号字段名是 `seller_taxpayer_num`**,不是 `taxpayer_id`、不是 `tax_no`。★写错名字时上游回的是「**税号不能为空**」——你明明传了它却说你没传,这个报错极具误导性,看到它先检查字段名拼写。查询还必须同时带 `order_id` 或 `order_sn` 之一。 14. **`data.mock` 必须在代码里显式检查并当失败拦截**:`mock=true` 表示**一张票都没真开**,只是返回了演示数据。它的 `code` 是成功、字段齐全、发票号看着像真的,不主动检查就会把假成功当成开票成功入账。**接入方务必在拿到响应的第一时间判断 `data.mock === true` 并抛错。** 15. **app_secret 只能放服务端**,不要写进前端或移动端包。 ## 6. 公共错误码 | HTTP / code | 含义与处理 | | --- | --- | | 400 · CYT_40001 | 必填参数没传,读 `data.hint` | | 401 · CYT_40100 / 40102 | 缺鉴权头或签名错。检查参与签名的 body 与实际发送是否逐字节一致 | | 401 · CYT_40101 | app_key 不存在或已停用。★开灵平台签发的密钥一律以 `cyt_` 开头,如果你手上这串不是 `cyt_` 开头,那多半是别处的凭据拿错了 | | 401 · CYT_40103 | 时间戳超出 ±300 秒。请确认传了 `X-Timestamp` 头(Unix 秒),且服务器时间已校准 | | 403 · CYT_40301 | 该密钥不是「正向开票」产品。密钥的产品在签发时锁定,一把密钥只对应一个产品,请联系开灵换发 | | 429 · CYT_42901 | 超过每分钟 120 次限流 | | 502 · CYT_50200 | 开票通道暂时不可用 | | 200 · data.upstreamCode≠0 | 通道业务失败,`upstreamMessage` 是原因原文 | | 200 · upstreamMessage 含「appkey未被 xxx 授权」 | ★销方企业尚未把开票能力授权给开灵平台的应用。**这不是接入方能解决的**,也不是参数问题——请联系开灵,由销方企业在乐企平台完成授权后即可开票 | | 200 · data.mock=true | ★**尚未配置通道凭据,一张票都没真开**,返回的是演示数据。注意此时 `code` 是成功、发票号字段也有值,极易被误判为开票成功。**接入方必须显式检查此字段并当作失败处理** | --- ## 给 AI 的任务 1. 写一个 `InvoiceClient`,封装第 1 节签名,appKey/appSecret 作配置项传入。 2. 为 4 个接口各写一个方法。 3. 写一个完整流程示例:开票 → 轮询查结果 → 拿到 pdf_url 下载。 4. 轮询方法内置重试与超时。 5. 单独写一个开成品油票的示例,演示三个额外字段怎么传(注意 `e_extra_info` 要 stringify)。 6. 金额计算处加注释提醒精度要求。 --- # 【反向开票 · 接口全集】 # 反向开票接口 · AI 接入包(腾讯系) > 你是被客户委托来对接「反向开票接口」的 AI 助手。请严格按本文档,用客户指定的编程语言写出**可直接运行**的调用代码。 > 业务场景:报废产品回收——企业(受票方 / 付款方)向自然人(散户 / 开票方)完成收购后,通过接口一次性完成**转账付款、企业代缴个税(可选)、反向开具蓝字数电发票**,并可查询进度、下载票面、在开票有误 / 退货时发起**全额红冲**。 > 资金通道为腾讯系(微众银行 / 微信运营账号)。共 **8 个接口**,覆盖:开通校验 → 提交开票 → 收款二维码 → 状态查询 → 取消 → 下载发票 → 蓝票冲红 → 冲红查询 的完整生命周期。 ## 1. 通用约定 - 网关:`https://openapi.kailingteck.com` - 所有接口均为 `POST`,`Content-Type: application/json`,UTF-8 编码。 - 响应统一信封:`{ "code": 0, "data": { ... }, "message": null }`。`code=0` 表示成功;`code≠0` 时 `message` 携带失败原因。 - **计费**:仅「提交转账和开票申请」按开票金额(`total_amount` 价税合计 × 合同费率)计费,受理成功才计费;其余 7 个接口(含蓝票冲红)**全部免费**。 ## 2. 鉴权(每次请求必带三个请求头) - `X-App-Key`:平台分配的 app_key - `X-Timestamp`:Unix **秒级**时间戳(与服务器时差需 ≤300 秒,否则报防重放) - `X-Sign`:签名 = `sha256(app_key + timestamp + 请求体原文 + app_secret)` 的**小写十六进制** ⚠️ 关键坑:签名里的"请求体原文"必须与实际发出的字节**完全一致**。正确做法是先把对象序列化成字符串,再拿**同一个字符串**既算签名、又作请求体,切勿序列化两次。 ### 签名与调用示例(Node.js 18+) ```js const crypto = require('crypto'); const APP_KEY = '你的app_key', APP_SECRET = '你的app_secret'; const BASE = 'https://openapi.kailingteck.com'; async function call(path, payload) { const body = JSON.stringify(payload); // 只序列化一次 const ts = Math.floor(Date.now() / 1000).toString(); const sign = crypto.createHash('sha256') .update(APP_KEY + ts + body + APP_SECRET).digest('hex'); const res = await fetch(BASE + path, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-App-Key': APP_KEY, 'X-Timestamp': ts, 'X-Sign': sign }, body, }); return res.json(); } ``` ### curl 示例(以查状态为例) ```bash BODY='{"order_no":"20260112000004"}' TS=$(date +%s) SIGN=$(printf '%s' "${APP_KEY}${TS}${BODY}${APP_SECRET}" | shasum -a 256 | cut -d' ' -f1) curl -X POST https://openapi.kailingteck.com/api/v1/invoice/reverse/transfer_status \ -H "Content-Type: application/json" \ -H "X-App-Key: ${APP_KEY}" -H "X-Timestamp: ${TS}" -H "X-Sign: ${SIGN}" \ -d "$BODY" ``` ## 3. 接口清单(推荐调用顺序) ### ① 查询企业开通状态 `POST /api/v1/invoice/reverse/company_query` 开票前置校验。回收企业(销方)须完成企业注册审核、乐企开通、微众银行开户三关。 - 请求:`taxpayer_no`(string,必填,企业统一社会信用代码) - 响应 data:`company_status`(1审核中/2已审核)、`leqi_status`(0未开通/1审核中/2已开通/4接入失败)、`bank_status`(0未开通/1审核中/2已开通/3需支付授权)、`bank_account`(微众银行账号,即开票时 buyer_bank_account 来源)、`issuer_list`[{ `issuer_id`, `name` }](开票员列表) - 开票硬前提:`company_status=2` 且 `leqi_status=2` 且 `bank_status=2` ```json // 请求 { "taxpayer_no": "914403001234567890" } // 响应 { "code": 0, "data": { "company_status": 2, "leqi_status": 2, "bank_status": 2, "bank_account": "999967735837012345678", "issuer_list": [{ "issuer_id": "1985991047072530434", "name": "张** 0012" }] }, "message": null } ``` ### ② 提交转账和开票申请 `POST /api/v1/invoice/reverse/transfer_apply` 【计费】 完成收购后,发起转账 + 蓝字发票开具。术语:buyer=回收企业=付款方=受票方;seller=自然人=收款方=开票方。 - 请求(顶层):`request_id`(幂等键,必填,唯一)、`buyer_taxpayer_no`、`buyer_bank_account`、`seller_name`、`seller_phone`、`seller_id_card`(自然人三要素必填)、`seller_bank_name`、`seller_bank_account`(选)、`invoicing_type`(3=报废收购专票/4=报废收购普票)、`total_amount`(价税合计)、`tax_amount`、`net_amount`、`issuer_id`(开票员,来自①)、`items`[]、`is_enterprise_tax`(企业代缴个税,选)、`transfer_channel`(0=微众银行/1=微信运营账号,选) - `items[]`:`category`、`tax_class_code`(税收分类合并编码)、`goods_short_name`、`goods_name`、`goods_specs_model`(选)、`goods_unit`、`goods_quantity`、`goods_price`、`goods_tax_rate`、`goods_total_amount` - 响应 data:`order_no`(订单号,后续查询/二维码/取消/下载/冲红均凭此) ```json // 请求 { "request_id": "32fea2b28b3c4caeac7cf6173c9c15da", "buyer_taxpayer_no": "914403001234567890", "buyer_bank_account": "9999677358370100212121", "seller_name": "张三", "seller_phone": "18568641234", "seller_id_card": "41234567890123456789", "invoicing_type": 4, "total_amount": 1.10, "tax_amount": 0.01, "net_amount": 1.09, "issuer_id": "1985991047072530434", "items": [{ "category": "废铜", "tax_class_code": "1110201000000000000", "goods_short_name": "废铜", "goods_name": "废铜", "goods_unit": "斤", "goods_quantity": 2, "goods_price": 0.55, "goods_tax_rate": 0.01, "goods_total_amount": 1.1 }] } // 响应 { "code": 0, "data": { "order_no": "20260112000004" }, "message": null } ``` `request_id` 由你方生成保证唯一,重复提交不会重复建单(幂等)。 ### ③ 获取转账和开票申请的二维码 `POST /api/v1/invoice/reverse/transfer_qrcode` 凭订单号获取**收款二维码**,供自然人微信扫码确认收款。 - 请求:`order_no`(必填) - 响应 data:`qrcode`(base64 PNG,前端可 `data:image/png;base64,` 直接渲染) ### ④ 查询转账和开票申请的状态 `POST /api/v1/invoice/reverse/transfer_status` 凭订单号查转账 / 缴税 / 开票各环节状态(建议轮询间隔 ≥30 秒)。 - 请求:`order_no`(必填) - 响应 data:`order_status`(1待付款/4已完成/5已取消/7已失效)、`tax_status`(1待缴税/2缴税中/3成功/4已取消)、`invoice_status`(1未开/2开具中/3成功/4失败/5已取消)、`invoice_no`、`pay_time`、`receipt_time`、`invoice_time`、`tax_time`、`invoice_error_message`、`transfer_error_message` - 终态成功 = `order_status=4` 且 `tax_status=3` 且 `invoice_status=3` ```json { "code": 0, "data": { "order_status": 4, "tax_status": 3, "invoice_status": 3, "invoice_no": "26367200000123456789", "pay_time": "2026-01-13 00:05:32", "invoice_time": "2026-01-13 00:05:56", "invoice_error_message": null, "transfer_error_message": null }, "message": null } ``` ### ⑤ 取消转账和开票申请 `POST /api/v1/invoice/reverse/cancel_transfer` 仅**缴税前**可取消;已缴税/已开票的改走蓝票冲红。 - 请求:`order_no`(必填) - 响应:`code=0` 即受理成功(`data` 为 null) ### ⑥ 下载发票 `POST /api/v1/invoice/reverse/invoice_download` 凭企业税号 + 发票号码,获取发票文件的下载链接。**该链接为本平台代理转存地址**(GET 直接下载,无需鉴权头,可交浏览器/下载器),有效期 24 小时。 - 请求:`taxpayer_no`(必填)、`invoice_no`(必填)、`file_type`(1=PDF/2=XML/3=OFD) - 响应 data:`file_url`(平台代理下载地址,24 小时内有效) ```json // 请求 { "taxpayer_no": "914403001234567890", "invoice_no": "26367200000123456789", "file_type": 1 } // 响应 { "code": 0, "data": { "file_url": "https://openapi.kailingteck.com/api/v1/invoice/reverse/file/__26367200000123456789.pdf" }, "message": null } ``` 拿到 `file_url` 后请及时 GET 下载并转存到你方系统归档,不要长期缓存该链接。 ### ⑦ 发起蓝票冲红 `POST /api/v1/invoice/reverse/red_invoice` 蓝字发票开具成功后,如遇开票有误 / 退货 / 中止 / 折让,发起红冲。**仅支持全额红冲**,凭原订单号发起。免费。 - 请求:`order_no`(必填)、`reason`(1开票有误/2销货退回/3服务中止/4销售折让,必填)、`issuer_id`(选) - 响应:`code=0` 即受理成功(`data` 为 null);进度用 ⑧ 查询 ```json { "order_no": "20260112000004", "reason": 1 } ``` ### ⑧ 查询冲红状态 `POST /api/v1/invoice/reverse/red_invoice_status` 凭原订单号查红字发票开具进度与红字发票号码。 - 请求:`order_no`(必填) - 响应 data:`invoice_no`(红字发票号码)、`invoice_status`(2开具中/3成功/4失败)、`invoice_time`、`invoice_error_code`、`invoice_error_message` ## 4. 错误处理 - 上游业务失败:信封 `code≠0`,按 `message` 提示修正后重试。 - 平台层拦截(信封为 `{ code: "CYT_xxx", msg, requestId, data }`): - `CYT_40001`(400)参数缺失/格式错误(如 total_amount 缺失) - `CYT_40100/40101`(401)缺鉴权头 / app_key 无效 - `CYT_40102`(401)签名校验失败(核对拼接顺序与 body 一致性) - `CYT_40103`(401)时间戳过期(±300 秒) - `CYT_42901`(429)请求过于频繁 - `CYT_42902`(402)账户余额不足 - `CYT_50200`(502)开票通道暂不可用,稍后重试 --- # 【反向开票 · 业务型接口】 # 工商银行反向开票(业务型接口) · AI 接入包 > 你是被客户委托来对接「工商银行乐企联用反向开票」的 AI 助手。 > 请严格按本文档,用客户指定的编程语言写出可运行的对接代码。不要臆造字段,文档里没有的字段一律不要传。 ## 0. 一句话架构 客户业务系统 → 调开灵这一套接口 → 开灵中台 → 工商银行融e聚(乐企联用)。 **工行那边的 RSA2 签名、AES-CBC 加密、SHA1 验签、两个 appId、页面类接口的表单跳转,全部由开灵中台承担,客户一概不用碰。** 客户只需要按第 1 节做一次简单签名,然后调普通的 JSON 接口。 接口分两类,处理方式完全不同: - **数据类**:POST 进 / JSON 出,直接读 `data`。 - **页面类**:POST 进,返回一个 `pageUrl`。前端 `location.href = pageUrl` 跳过去,用户在**工行托管的页面**上完成扫码 / 刷脸 / 付款。**不要自己做这些页面,也不要试图解析 pageUrl。** ## 1. 鉴权与签名(所有接口一样) 每次请求带三个头: | 请求头 | 值 | | --- | --- | | `X-App-Key` | 开灵签发的 app_key | | `X-Timestamp` | Unix **秒级**时间戳,与服务器时差需在 ±300 秒内 | | `X-Sign` | `sha256(app_key + timestamp + 请求体原文 + app_secret)` 的**小写十六进制** | **签名务必注意**:参与 sha256 的"请求体"必须与实际发送的 body **逐字节一致**(含空格)。 最稳的写法是:先把 body 序列化成一个字符串变量,签名和发送都用这同一个变量。 ```js const body = JSON.stringify(payload); // 固定下来这个字符串 const ts = Math.floor(Date.now() / 1000).toString(); const sign = sha256hex(appKey + ts + body + appSecret); fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-App-Key': appKey, 'X-Timestamp': ts, 'X-Sign': sign, }, body }); // ← 用同一个 body ``` ## 2. 返回信封(所有接口一样) ```json { "code": "CYT_00000", "msg": "success", "requestId": "cyt_xxx", "timestamp": "…", "data": { } } ``` `code` 为 `CYT_00000` 表示**调用成功**。业务字段都在 `data` 里。 ⚠ **调用成功 ≠ 业务通过**。例如「转账并开票」返回 `code=CYT_00000` 但 `data.canInvoice=false`,表示这个散户撞了额度红线、本单不能开。**必须再判断 data 里的业务字段。** ## 3. 调用顺序(照这个顺序写代码) ``` 【企业一次性】① 企业开通 → 法人用电子税务局App扫码授权 → 手动下载一次发票激活额度 【散户一次性】② 散户入驻·第一步 发起扫脸(拿 pageUrl 去刷脸,存下 transNo) 【散户一次性】②之二 散户入驻·第二步 建收款人档案(报姓名/身份证/银行卡)★刷脸≠入驻,这一步不做后面开票必被退 → ③ 查散户状态(轮询到 canInvoice=true) 【每卖一票】 ④ 转账并开票(过额度闸门,拿 payUrl)→ 老板付款(扳机) → 工行自动开票+缴税+打款 → ⑤ 查开票结果(轮询到 status=done,可顺带取票文件) 【按需】 ⑥ 散户改卡/注销 ⑦ 红冲/作废 ⑧ 下载发票 ``` **跨接口传递的三个关键值,一定要存下来:** | 值 | 哪来的 | 哪要用 | | --- | --- | --- | | `userId` | 你自己生成的散户唯一ID | ②③④⑥ 全都用它当钥匙 | | `transNo` | ② 散户入驻返回 | ③ 查实名结果必须用它 | | `orderId` | 你自己生成的订单号 | ④⑤⑦⑧ 全流程主键 | ## 4. 接口详情(8 个) ### ① 企业开通 回收企业(付款开票方)首次开通:拿到工行税务授权的跳转链接,企业法人去电子税务局扫码授权。一次性、超低频。页面类接口。 #### ① 企业开通 `POST https://openapi.kailingteck.com/api/v1/rinv/enterprise/open` (页面类) **请求参数** | 字段 | 必填 | 说明 | | --- | --- | --- | | `vendorId` | 必填 | 服务商ID(合作机构编号) | | `leqiId` | 选填 | 乐企机构号 | | `authType` | 选填 | 授权人类型:01 法定代表人 / 02 财务负责人 | | `authNo` | 选填 | 授权人证件号 | | `jumpUrl` | 选填 | 授权完成后回跳你们页面的地址 | **请求示例** ```json { "vendorId": "V1", "leqiId": "L1", "authType": "01", "authNo": "110101199001011234", "jumpUrl": "https://你们的页面" } ``` **返回参数(data 内)** | 字段 | 说明 | | --- | --- | | `pageUrl` | 工行税务授权页的地址。前端直接跳过去,页面上有二维码给法人扫 | | `mock` | true=尚未配置工行凭据、返回的是演示数据;false=真调了工行 | **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "requestId": "cyt_xxx", "data": { "pageUrl": "https://openapi.kailingteck.com/api/v1/icbc/redirect/rxxx", "mock": false } } ``` **异常与处理** | 情形 | 含义与处理 | | --- | --- | | 跳转页打开显示 MOCK 字样 | 尚未配置工行凭据,联系开灵配置后即为真实工行页面 | | 法人扫码后提示无权限 | 扫码的人不是法定代表人或财务负责人,或没在电子税务局 App 里登录本企业 | **前端怎么用** ```js location.href = res.data.pageUrl; // 把法人带到工行托管的授权页 ``` > 法人必须用「电子税务局」App 扫码(微信、工商银行 App 都扫不了)、刷脸、设授权有效期。授权完还要在电子税务局里手动下载一次发票,自动开票额度才生效——这一步最容易漏,漏了后面开票会一直失败。 --- ### ② 散户入驻(发起扫脸) 自然人(卖废品的散户)入驻第一步:发起工行刷脸实人认证,拿到扫脸链接给散户本人刷脸。页面类接口。 #### ② 散户入驻(发起扫脸) `POST https://openapi.kailingteck.com/api/v1/rinv/seller/onboard` (页面类) **请求参数** | 字段 | 必填 | 说明 | | --- | --- | --- | | `userId` | 必填 | 你侧散户唯一ID(后续所有接口都用它当钥匙) | | `name` | 必填 | 散户姓名 | | `idNo` | 必填 | 身份证号 | | `phone` | 选填 | 手机号(认证过程中发短信验证码) | | `jumpUrl` | 选填 | 刷脸完成后回跳地址 | **请求示例** ```json { "userId": "U123", "name": "张三", "idNo": "110101199001011234", "phone": "13800000000", "jumpUrl": "https://你们的页面" } ``` **返回参数(data 内)** | 字段 | 说明 | | --- | --- | | `pageUrl` | 工行实人认证(刷脸)页地址,让散户本人打开 | | `transNo` | 本次认证的流水号。务必存下来——查③散户状态时要用它取实名结果 | | `mock` | true=演示数据;false=真调了工行 | **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "requestId": "cyt_xxx", "data": { "pageUrl": "https://openapi.kailingteck.com/api/v1/icbc/redirect/rxxx", "transNo": "T1786630352940", "mock": false } } ``` **异常与处理** | 情形 | 含义与处理 | | --- | --- | | 散户刷脸失败 | 常见原因:光线不足、动作不到位、本人信息与身份证不一致。可重新调本接口再发起一次 | | 没存 transNo | 查实名结果只能靠 transNo,丢了就得让散户重新刷一次脸 | **前端怎么用** ```js location.href = res.data.pageUrl; // 散户去工行页面刷脸 save(res.data.transNo); // 存下 transNo,查③状态要用 ``` > 扫脸是异步的:散户在工行页面刷完,你再用 transNo 调③查实名结果,不会自动推回来。 --- ### ②之二 建收款人档案(入驻第二步) 散户刷脸通过之后必须再做这一步:把散户的姓名、身份证、收款银行卡报给工行,在回收企业名下建成「收款人」。★刷脸只是实名认证,不等于已入驻 —— 不做这一步,工行侧根本没有这个收方,后面开票会被退回。数据类接口。 #### ②之二 建收款人档案(入驻第二步) `POST https://openapi.kailingteck.com/api/v1/rinv/seller/register` (数据类) **请求参数** | 字段 | 必填 | 说明 | | --- | --- | --- | | `userId` | 必填 | 散户ID,必须与②发起扫脸时传的同一个 | | `name` | 必填 | ★收款人姓名。必须与身份证、银行卡户名三者完全一致,对不上工行会返「姓名和身份证信息不匹配」 | | `idNo` | 必填 | 身份证号 | | `phone` | 必填 | 散户手机号 | | `bankCard` | 必填 | 散户收款的银行卡号。受理成功后平台会记住它,「③查散户状态」查入驻结果要用 | | `isIcbcCard` | 必填 | 是不是工行卡:true=工行卡 / false=其他银行卡 | | `accountCode` | 选填 | 与 isIcbcCard 等价的工行原字段:1=工行卡 / 0=非工行卡,传了直接透传给工行;与 isIcbcCard 同时传时以 isIcbcCard 为准 | | `bankName` | 非工行卡必填 | 开户行名称,如「中国建设银行重庆巴南支行」 | | `bankCode` | 非工行卡必填 | 开户行联行号(12 位) | | `idStartDate` | 必填 | 身份证签发日期,格式 yyyy-MM-dd | | `idEndDate` | 必填 | 身份证截止日期 yyyy-MM-dd;永久有效填 9999-12-30 | | `occupation` | 散户无工行账户时必填 | 职业代码:1公务员/2事业单位/3公司员工/4军警/5工人/6农民/7管理/8技术/9私营业主/10文体明星/11自由职业/12学生/13无职业/14其他/15退休 | | `address` | 散户无工行账户时必填 | 常用住址,不少于 4 个汉字 | | `vendorId` | 选填 | 企业子商户编号。不传自动取后台「工行参数」里为贵司配置的值 | | `businessType` | 选填 | 业务类型。不传自动取后台配置(默认 0006 开票业务)。★0004 再生资源、0005 农产品收购 这两个取值会被工行以「平台商户未开启智慧清分」拒掉,0006 不受此限 | **请求示例** ```json { "userId": "NP333", "name": "唐雪妮", "idNo": "500113199301287129", "phone": "18523176826", "bankCard": "6212260200000000000", "isIcbcCard": true, "idStartDate": "2015-06-18", "idEndDate": "2035-06-18", "occupation": "6", "address": "重庆市巴南区龙洲湾街道" } ``` **返回参数(data 内)** | 字段 | 说明 | | --- | --- | | `accepted` | ★先看这个:true=工行已受理建档申请(含「工行侧本来就有档案」的情况);false=没受理,看 reason | | `alreadyExists` | true=工行侧本来就有这个收方(已入驻或审核中),本次已自动补录平台侧档案映射。对历史/旁路建档的散户重放本接口即可对齐数据,幂等、无副作用 | | `userId` | 回显你传的散户ID | | `autoAudited` | true=工行自动审核已通过;false=还要工行分行人工审核 | | `applyTime` | 申请入驻时间 | | `auditTime` | 审核通过时间(还没审就没有) | | `reason` | accepted=false 时才有,工行给的原始说法 | | `hint` | 下一步该干什么 | | `raw` | 工行原始返回,排查时看这个 | **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "data": { "userId": "NP333", "accepted": true, "autoAudited": false, "applyTime": "2026-09-15 17:20:31", "hint": "已受理,等工行审核。用 ③ 查散户状态轮询 onboarded 字段", "mock": false } } ``` **返回示例(失败 / 业务拦截)** ```json // 姓名与身份证对不上(最常见) { "code": "CYT_00000", "msg": "success", "data": { "userId": "NP333", "accepted": false, "reason": "30601019#&姓名和身份证信息不匹配", "raw": { "return_code": "30601019" } } } ``` **异常与处理** | 情形 | 含义与处理 | | --- | --- | | 30601019 姓名和身份证信息不匹配 | ★姓名、身份证号、银行卡户名三者必须完全一致。测试时用假身份证必然报这个 | | 30601008 / 30601009 / 30601013 已入驻或审批中 | ★不算失败:接口已幂等化,这三种情况返回 accepted=true + alreadyExists=true,并自动补录平台侧档案映射(编号↔卡号)。对已在工行建档的散户重放本接口,即可让平台与工行数据对齐 | | 30601004 平台商户未开启智慧清分 | 业务类型传成了 0004 或 0005。本场景用 0006,不传即默认 0006 | | 30601001 入参有误,必输项为空 | 缺必填项。重点查 appIdSub(企业子商户编号)有没有配、bankName/bankCode 非工行卡时有没有传 | | 缺少 name / idNo / bankCard | 返回 400 CYT_40001,data.hint 写了缺哪个 | | 非工行卡没传开户行 | 返回 400,非工行卡必须同时传 bankName 与 bankCode;工行卡请传 isIcbcCard=true | **前端怎么用** ```js const r = await post('/api/v1/rinv/seller/register', body); if (!r.data.accepted) return alert(r.data.reason); // 没受理,按 reason 让用户改 // 受理了 → 轮询 ③ 查散户状态,直到 onboarded 变成 true ``` > ★这一步和②发起扫脸是两件事,都要做,顺序是先②刷脸、再②之二建档。刷脸解决「这个人是不是本人」,建档解决「工行认不认这个收款人、钱往哪张卡打」。只做②不做②之二,工行侧不存在这个收方,④开票会被退回。建档提交后不是立刻生效,要工行审核,用 ③ 查散户状态 轮询 onboarded 字段。平台会自动补齐 appId、appIdSub、accountKind=02 个人、receiverType=03 自然人、idType=0 身份证、以及审核结果通知地址,这些不用你传。 --- ### ③ 查散户状态 一次拿到某散户的全景:实名过没、500 万额度、收款人建档/审核/授权/冻结,并直接告诉你现在能不能给他开票。数据类接口。 #### ③ 查散户状态 `POST https://openapi.kailingteck.com/api/v1/rinv/seller/status` (数据类) **请求参数** | 字段 | 必填 | 说明 | | --- | --- | --- | | `userId` | 必填 | 散户ID(与②一致),工行口径里这是「收方编号」 | | `vendorId` | 选填 | 企业子商户编号,工行口径里这是「付方编号」。不传则自动取后台为贵司配置的值 | | `transNo` | 选填 | ②发起扫脸时平台已自动记住流水号,一般不用传;显式传则以传的为准 | | `taxNo` | 选填 | 企业税号。不传时自动取后台为贵司配置的税号,一般不用传 | | `bankCard` | 选填 | 散户收款卡号。查「是否已入驻」工行必须有卡号——②之二建档成功时平台已自动记住,一般不用传;平台没有记录(如建档没走本平台)时才需显式传 | **请求示例** ```json { "userId": "U123", "vendorId": "V1", "transNo": "T1786630352940", "taxNo": "91500114MAET09070Q" } ``` **返回参数(data 内)** | 字段 | 说明 | | --- | --- | | `canInvoice` | ★直接看这个:true=现在可以给他开票。等于「已实名 且 已完成收方入驻 且 没触额度红线」。2026-09-15 起把「已入驻」也纳入判定 —— 此前只看实名,收方其实没在工行入驻时也会返 true,属误报 | | `onboarded` | ★是否已在工行完成收方入驻。注意:实名通过 ≠ 已入驻,这是两件事,刷脸只是其中一步 | | `blockedBy` | canInvoice=false 时卡在哪一步:realname 未实名 / onboard 未入驻 / quota 撞额度红线 | | `blockedReason` | 同上,中文说明,可直接展示给用户 | | `receiverError` | 查收方档案时工行返回的原始报错(正常时为 null)。出现 30601004「平台商户未开启智慧清分」属平台侧能力未开通,需联系工行处理 | | `realNamed` | 宽口径实名:刷脸通过 或 工行收方档案已存在(档案存在即推定实名,此时 realNamedBy=record)。★合规判断「散户本人是否做过实名」别用它,用 faceVerified | | `faceVerified` | ★真实名:散户本人刷脸实测通过(工行 authResult=02)。false=没刷过脸或没刷成——收方档案存在也不算。合规口径只认这个字段 | | `faceTransNo` | 本次实名查询用的认证流水号(显式传的或平台记住的);null=两边都没有,刷脸结果无从查起 | | `receiverStatus` | 收款人状态 | | `auditStatus` | 工行分行的审核状态 | | `authorizeStatus` | 授权状态 | | `freezeStatus` | 冻结状态 | | `quota.over500W` | 连续 12 个月累计开票是否超 500 万:0 否 / 1 是。★硬红线,超了不能开 | | `quota.overYear210W` | 当年累计是否超 210 万:0 否 / 1 是。★硬红线 | | `quota.overYear200W` | 当年累计是否超 200 万:0 否 / 1 是。接近提醒,不阻断 | | `quota.overMonth10W` | 当月累计是否超 10 万:0 否 / 1 是。接近提醒,不阻断 | | `quota.backAmount` | 该散户是否有退税余额:0 否 / 1 是 | | `raw` | 工行三个原始接口的全字段(实名结果 / 收款人 / 额度),需要细节时看这里 | **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "data": { "userId": "U123", "canInvoice": true, "realNamed": true, "receiverStatus": "0", "auditStatus": "2", "authorizeStatus": "1", "freezeStatus": "1", "quota": { "over500W": "0", "overYear200W": "0", "overYear210W": "0", "overMonth10W": "0", "backAmount": "0" }, "mock": false, "raw": { } } } ``` **返回示例(失败 / 业务拦截)** ```json // 还没实名 / 触了红线,canInvoice 就是 false { "code": "CYT_00000", "data": { "userId": "U123", "canInvoice": false, "realNamed": false, "quota": { "over500W": "1" } } } ``` **异常与处理** | 情形 | 含义与处理 | | --- | --- | | canInvoice 一直 false | 先看 realNamed:false 说明散户没刷脸或没刷过;true 但仍 false,就是撞了 quota 里的硬红线 | | quota 全为空 | 没传 taxNo,或该散户还没实名——额度查询要靠实名后绑定的关系 | **前端怎么用** ```js // 轮询,直到 data.canInvoice === true 再给他开票 if (!res.data.canInvoice) return alert('该散户暂不可开票'); ``` > ★依赖铁律:查 500 万额度用 userId 当钥匙、不收身份证——所以必须先完成②扫脸实名,把 userId 和身份证绑上,才查得到额度。 --- ### ④ 转账并开票(核心) 核心动词。传订单 + 散户,内部先过「查额度闸门」,过了才给付款链接;老板付款后,工行自动完成开票 + 缴税 + 打款给散户。 #### ④ 转账并开票(核心) · 转账并开票 `POST https://openapi.kailingteck.com/api/v1/rinv/invoice/create` (数据类) **请求参数** | 字段 | 必填 | 说明 | | --- | --- | --- | | `orderId` | 必填 | 你侧订单号(全流程唯一,查结果、红冲、作废都用它) | | `userId` | 必填 | 收款散户ID。对应工行的「收方编号」outVendorId —— 反向开票里收钱的是散户 | | `vendorId` | 选填 | 企业子商户编号。对应工行的「付方编号」outUserId —— 付钱的是回收企业。不传或传空时自动取后台「工行参数」里为贵司配置的值,所以一般不用传 | | `taxNo` | 选填 | 企业税号 | | `trxChannel` | 选填 | 交易渠道:01 PC / 02 移动端 / 03 H5 | | `invoiceType` | 必填 | ★发票类型:01=增值税专用发票 / 02=普通发票。工行「开票时必输」,由你按实际业务选择。收购发票(农产品/报废产品)专票、普票两种场景都有,我方不设默认、不代填,你传什么就是什么,不传即不上送 | | `payChannel` | 选填 | 支付渠道:不传时默认 05=公对私结算(工行确认本场景固定用 05)。走 05 时机构编码 verifiedCode 与 U盾 ukeyId 都不用传 | | `asynFlag` | 选填 | ★支付标识,钱怎么付,与 payChannel 是两件事:不传时默认 1=推送至企业网银,由网银经办工号完成付款与确认;2=直接支付;0=其他。走 1 时必须同时传 payerName 与 payerAcctNum | | `buyerInvTypeCode` | 选填 | ★票种:不传时默认 04=报废产品收购发票。农产品收购请显式传 01。与 specificElements 成对(04↔24、01↔16),传其中一个另一个会自动推导 | | `merBusiModel` | 选填 | 平台经营模式:不传时默认 02=非平台自营(回收企业自己是买方、开灵只是技术服务商)。01=平台自营 / 03=反向开票助手 | | `areaCode` | 选填 | 区域代码。工行只在 merBusiModel=01 平台自营时要求必输,默认的 02 模式下不必传,我方也不代填;确需上送时自行传值(01 模式下不传则自动取后台为贵司配置的 proTaxAuthCode) | | `jumpUrl` | 选填 | 付款完成后回跳地址 | | `notes` | 选填 | ★票面备注,最多 450 字。写什么就印在发票的「备注」栏里(合同号、结算单号、产地等)。也可以写成 remark,两个名字都认 | | `enterpriseConfirm` | 选填 | ★谁来点确认:不传时默认 1=企业代散户确认(确认动作在企业网银经办工号处完成,散户不用再点,工行直接显示「订单已受理」);要改回散户本人确认,显式传 0 | | `taxPaymentMethod` | 选填 | ★谁去交税(只有再生资源/报废产品收购能用):不传或 1=企业委托扣缴(默认,企业付全款,工行代扣税后把净额打给散户);0=散户自行办理(工行不代扣,企业只付货款,税由散户自己去办) | | `iitProject` | 选填 | taxPaymentMethod=0 时配套传:个税项目 1=财产转让所得 / 2=经营所得 | | `supplementaryTax` | 选填 | taxPaymentMethod=0 时配套传:补缴税费标志,工行文档说明目前固定传 Y | | `payerName` | 选填 | ★付款账户的户名,也就是钱从哪家公司的账户出。asynFlag=1(默认那种)时工行要求必填,最多 120 字 | | `payerAcctNum` | 选填 | ★付款账户的账号。asynFlag=1(默认那种)时工行要求必填,最多 34 位。务必用字符串传,写成数字会丢位数 | **请求示例** ```json { "orderId": "ORD1", "userId": "U123", "vendorId": "V1", "taxNo": "91500114MAET09070Q", "trxChannel": "03", "jumpUrl": "https://你们的页面", "notes": "合同号 HT2026-0912 / 产地 重庆", "enterpriseConfirm": "1", "taxPaymentMethod": "0", "iitProject": "2", "supplementaryTax": "Y", "asynFlag": "1", "payerName": "蜀衡铜业(重庆)有限公司", "payerAcctNum": "3100021109000000000" } ``` **返回参数(data 内)** | 字段 | 说明 | | --- | --- | | `canInvoice` | ★先看这个:true=闸门过了,payUrl 可用;false=撞了额度红线,本单不能开 | | `orderId` | 回显你传的订单号 | | `payUrl` | canInvoice=true 时才有。工行付款页地址,★老板在这一页付款即触发开票 | | `reason` | canInvoice=false 时才有,说明被拦的原因 | | `quota` | canInvoice=false 时才有,给出额度详情便于排查是哪条红线 | | `mock` | true=演示数据;false=真调了工行 | **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "data": { "orderId": "ORD1", "canInvoice": true, "payUrl": "https://openapi.kailingteck.com/api/v1/icbc/redirect/rxxx", "mock": false } } ``` **返回示例(失败 / 业务拦截)** ```json // 撞了额度红线:接口本身成功(code 仍是 CYT_00000),但 canInvoice=false { "code": "CYT_00000", "msg": "success", "data": { "orderId": "ORD1", "canInvoice": false, "reason": "额度红线,不可开票", "quota": { "blockNegInvPerMarker": "1", "curYearOverSpend210W": "0" } } } ``` **异常与处理** | 情形 | 含义与处理 | | --- | --- | | 接口返回成功了,但一直查不到票 | ★本接口返回成功只代表「我们把工行的表单准备好了」,工行此刻还没收到这一单。工行是在有人真正打开 payUrl 那一刻才收到的。没人打开,⑤查结果就会一直返「没有查到票据信息」,这是正常的,不是接口故障 | | 付款页打开后显示「链接已失效」 | ★那是我们的失效页不是工行的页面。payUrl 一次性、10 分钟有效,超时就要重新调本接口拿新的 | | 工行提示「增值税税率不合法」 | ★taxRate 要填百分数的数值,不是小数:1% 填 1、3% 填 3、13% 填 13。填 0.01 必被拒。工行原文佐证:「增值税税率传 3 时,unuseReduceTaxCode 必输」,那个 3 指的就是 3% 征收率 | | 金额相关的校验不过 | ★三条要自洽:商品明细里 price × goodsNum = goodsAmt;各商品 goodsAmt 之和 = orderAmount;orderAmount 是价税合计,小数位不超过 2 位。除不尽时优先调整金额,不要靠拉长单价小数位去凑 | | canInvoice=false | ★不是报错。是这个散户撞了 500 万 / 当年 210 万红线,本单不能开,读 reason 给用户提示 | | 缺少 orderId | 返回 400 CYT_40001,orderId 是全流程主键,必须传 | | 付款页打不开 | 检查 payUrl 是否已过期(一次性、10 分钟有效),过期就重新调本接口 | | 票开出来备注栏是空的 | ★检查开票时有没有传 notes(或 remark)。备注不会自动生成,不传就是空的;最多 450 字,超长会被工行截断 | | 散户一直卡在「待确认」 | ★没传 enterpriseConfirm,默认要散户本人去工行页面点确认。想让企业代确认、跳过散户这一步,传 enterpriseConfirm="1" | | 税还是被工行代扣了 | ★没传 taxPaymentMethod=0,默认就是企业委托扣缴。要散户自己交税,传 "0" 并配套 iitProject、supplementaryTax;结果在工行每日缴税对账单里按「缴纳方式」核对 | | 工行提示「参数校验失败:参数无效或非法」 | ★先查区域代码 areaCode。工行规格里它在「平台经营模式=01 平台自营」时必输,而默认就是 01;不传时我们会自动填后台配置的省市级税务机关代码。其次查商品明细的 price × goodsNum 是否等于 goodsAmt | | 付款页打开后工行提示参数错误 / 直接退回 | ★对照工行预下单必输项自查,最常见的三个:收方编号(不传我们会用后台配置兜底)、币种(平台自动注入 001)、商品明细里的含税单价 price(必须你们传,我们无法代算) | | 工行提示付款账户为空 / 付款页选不了账户 | ★传上 payerName(户名)与 payerAcctNum(账号)。默认走的就是企业网银异步支付(asynFlag=1),这种方式工行必须知道钱从哪个对公账户出,不传就走不下去 | | 付款账户账号少了几位 / 对不上 | ★payerAcctNum 要用字符串传。银行账号 19~34 位,写成 JSON 数字在解析时就已经丢精度,传到工行的是错号 | **前端怎么用** ```js if (res.data.canInvoice) location.href = res.data.payUrl; // 跳工行付款页 else alert(res.data.reason); // 额度红线,先别开 ``` > 税额由工行/税局乐企算好,付款后从⑤查结果里取回,中台不自算税。付款是整个开票流程的扳机——付款成功,工行才会开票、缴税、打款。两个开关各管各的:enterpriseConfirm 管「谁点确认」,taxPaymentMethod 管「谁去交税」,互不影响,可以单独传也可以一起传。工行没有单独的「二维码接口」,把 payUrl 转成二维码给散户扫就是通用做法;注意 payUrl 一次性、10 分钟有效,二维码页要能刷新。★发票类型 invoiceType 我方不再代填(2026-09-15 起):此前默认按 01 上送,客户并不知情,导致税率反复被判不合法。票种属业务定性,收购发票专票、普票两种场景都存在,请按实际业务自行上送。★平台替你填了哪些工行必输项(你都不用传,传了以你的为准):appId、invoiceNotifyUrl(回调地址)、currency(固定 001)、mac 与 trxIp(纯风控技术字段,网关代填本服务器的网卡地址与公网 IP,业务方不用关心)、outVendorId(取后台为你配置的收方编号)、areaCode(仅 merBusiModel=01 平台自营时才代填)。默认口径(不传时):票种按报废产品收购 04/24、平台经营模式按 02 非平台自营、支付渠道按 05 公对私结算、支付标识按 1 推送至企业网银、确认方式按 1 企业代确认。机构编码 verifiedCode 与 U盾 ukeyId 平台不代填,走 05 时工行也不要求,请求里带了就原样透传。★必须你传、平台补不了的:orderId、userId、goodsInfo(其中 goodsSeqno / projectName / goodsNum / goodsAmt / price / units 六项工行都标必输,少一项工行就退回;taxRate 开票时也必输,且要填百分数的数值(1% 填 1,不是 0.01))、orderAmount、以及走 asynFlag=1 推送企业网银时的 payerName 与 payerAcctNum。★工行页面侧逐字段实测(2026-09-21,比规格表更严)还会要求:payPhoneno(付款人手机号)、isSellerPersonProduct(是否销售自然人使用过的报废产品,N/Y)、iitProject(个税项目)与 supplementaryTax(补缴税费标志 Y)、payerName / payerAcctNum(付款账户)、taxPayerAccountNo / taxPayerBankCode / taxPayerOrgName(缴税付款账户三件套)——缺任何一个,付款页会直接显示「XX不可为空」。**其中付款与缴税账户这 6 项属企业级固定信息,2026-09-22 起改为平台按你的客户档案自动代填,你不用每单传**(后台「工行参数」里配对公账号、联行号、户名、付款人手机号即可);多账户场景想临时换账户,显式传这些字段即可覆盖。付款方式与付款账户(asynFlag / payerName / payerAcctNum)是工行原字段,原样传即可:默认按企业网银异步支付处理,这种方式下工行要求把付款账户的户名和账号一起报上来,否则付款页拿不到扣款账户。 #### ④ 转账并开票(核心) · 获取付款页(续付) `POST https://openapi.kailingteck.com/api/v1/rinv/invoice/pay` (页面类) **请求参数** | 字段 | 必填 | 说明 | | --- | --- | --- | | `orderId` | 必填 | ④下单时的订单号 | | `userId` | 选填 | 收款散户ID(工行口径「收方编号」),一般不用传,工行按订单号定位 | | `vendorId` | 选填 | 企业子商户编号,不传自动取后台为贵司配置的值 | | `trxChannel` | 选填 | 交易渠道:01 PC(默认,财务一般在电脑上付) / 02 移动端 / 03 H5 | | `jumpUrl` | 选填 | 支付完成后的返回页面地址 | **请求示例** ```json { "orderId": "ORD1" } ``` **返回参数(data 内)** | 字段 | 说明 | | --- | --- | | `payUrl` | ★企业支付页地址。一次性、10 分钟有效,交给付款经办人打开,或转成二维码 | | `orderId` | 回显订单号 | **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "data": { "orderId": "ORD1", "payUrl": "https://openapi.kailingteck.com/api/v1/icbc/redirect/xxxx", "mock": false } } ``` **前端怎么用** ```js const r = await post('/api/v1/rinv/invoice/pay', { orderId: 'ORD1' }); location.href = r.data.payUrl; // 或把 payUrl 转成二维码给付款经办人扫 ``` > 用途:④返回的付款页一次性、10 分钟有效——页面关掉/超时后不必重新下单,拿同一个订单号调这里重新生成企业支付页面继续付款。排错三句话:「10102705 没有查到票据信息」=④的付款页从没被真实打开过,订单还没送达工行,这时只能先走④返回的 payUrl;「10101901 订单支付中或已支付成功」=这单不用再付,直接用⑤查结果;「10102706 发票未预开票成功」=预开票没完成,先用⑤查单据状态。机构编码 verifiedCode 与 U盾 ukeyId 工行标「场景支付时必输」,走默认的 05 公对私结算不需要,平台不代填,传了原样透传。 --- ### ⑤ 查开票结果 把工行的 5 个原始状态翻成一个业务态,给你发票号 + 税额;带 withInvoice=true 时顺带把票文件也取回来。数据类接口。 #### ⑤ 查开票结果 `POST https://openapi.kailingteck.com/api/v1/rinv/invoice/status` (数据类) **请求参数** | 字段 | 必填 | 说明 | | --- | --- | --- | | `orderId` | 必填 | 你侧订单号(与④一致) | | `withInvoice` | 选填 | true=顺带把发票文件一起取回 | | `invoiceFormat` | 选填 | 票文件格式:1 ofd / 2 pdf / 3 xml(默认 2) | | `vendorId` | 选填 | 服务商ID | | `userId` | 选填 | 散户ID | **请求示例** ```json { "orderId": "ORD1", "withInvoice": true, "invoiceFormat": "2" } ``` **返回参数(data 内)** | 字段 | 说明 | | --- | --- | | `status` | ★业务状态。waiting_confirm 待散户确认 → waiting_pay 待付款 → invoicing 开票中 → done 已完成;failed 开票失败;cancelled 已取消(⑦取消开票之后的终态);done_upload_failed 开票完成但上传税局失败(票文件下载不到,看顶层 uploadFailReason,修复后调⑧的「上传重试」);processing 处理中 | | `invoiceCode` | 发票号码(开出来才有) | | `taxAmount` | 合计税额(工行/税局算好的,不是我们算的)——这是发票票面上的增值税税额 | | `taxRealAmount` | ★实缴税费合计:工行实际代扣的全部税费总额,通常大于等于票面税额 taxAmount,差额是个税等非票面税费。财务对账用它 | | `actuallyReceivedAmount` | ★散户实际到手金额(付款金额扣完实缴税费后的净额,以工行返回为准) | | `invoiceLevyItemResponse` | ★税费征收分项明细(数组):每项含征收项目/品目名称与代码、计税依据 taxBasis、税率 taxRate、应缴税额 taxPayable、应补退税额 taxCreditPayable、税款所属期。回答「这张票各税种各扣了多少」就靠它 | | `payAmount` | 合计金额(票面含税) | | `states.confirm` | 工行原始:自然人确认状态 00 未确认 / 01 自然人已确认 / 02 全部确认完成 | | `states.pay` | 工行原始:支付状态 02 成功 | | `states.invoice` | 工行原始:开票状态 00 初始 / 01 开票中 / 02 成功 / 03 失败 | | `states.upload` | 工行原始:发票上传状态 04 上传成功 | | `states.tax` | 工行原始:缴税状态 04 缴税成功 | | `invoice.byteData` | base64 编码的票文件(只有 withInvoice=true 且已开票成功时才有) | | `invoice.format` | 票文件格式 | | `raw` | 工行预查询接口的原始全字段 | **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "data": { "orderId": "ORD1", "status": "done", "invoiceCode": "26367200000123456789", "taxAmount": "42.81", "payAmount": "5018.35", "states": { "confirm": "02", "pay": "02", "invoice": "02", "upload": "04", "tax": "04" }, "invoice": { "byteData": "JVBERi0xLjQK……(base64)", "format": "2" }, "mock": false } } ``` **返回示例(失败 / 业务拦截)** ```json // 开票失败 { "code": "CYT_00000", "data": { "orderId": "ORD1", "status": "failed", "states": { "invoice": "03" } } } ``` **异常与处理** | 情形 | 含义与处理 | | --- | --- | | status 长期停在 waiting_confirm | 散户还没在工行侧确认这一票,需要引导散户去确认 | | status 长期停在 waiting_pay | 老板还没付款。付款是扳机,不付不开票 | | status=failed | 看 raw 里工行给的失败原因。常见:企业忘了在电子税务局手动下载一次发票、额度不足 | | status=done_upload_failed / states.upload=05 | ★票开出来了但上传税局失败,票文件下载不到。看顶层 uploadFailReason;典型:「他用或联用企业开票人未授权开票」=开票人没在电子税务局做乐企授权——授权补齐后调⑧的「上传重试」重新上传即可,不用重新开票 | | invoice 字段没有 | 要么没传 withInvoice=true,要么还没开票成功 | **前端怎么用** ```js // 轮询直到 data.status === 'done' if (res.data.status === 'done') download(res.data.invoice.byteData); ``` > 税额是工行算好后取回的,平台不参与计算。五个原始状态都放在 states 里,排查时看它比看 status 更细。 --- ### ⑥ 散户改卡 / 注销 散户入驻后的维护:换收款银行卡、或注销(解绑收款人)。两个数据类接口。 #### ⑥ 散户改卡 / 注销 · 改卡 `POST https://openapi.kailingteck.com/api/v1/rinv/seller/update-card` (数据类) **请求参数** | 字段 | 必填 | 说明 | | --- | --- | --- | | `userId` | 必填 | 散户ID | | `vendorId` | 必填 | 服务商ID | | `bankCard` | 必填 | 新的收款卡号 | | `bankName` | 选填 | 开户行名称(非工行卡时填) | | `bankCode` | 选填 | 开户行联行号(非工行卡时填) | **请求示例** ```json { "userId": "U123", "vendorId": "V1", "bankCard": "6222000000000000", "bankName": "中国银行苏州分行", "bankCode": "104305000000" } ``` **返回参数(data 内)** | 字段 | 说明 | | --- | --- | | `userId` | 回显散户ID | | `result` | 工行给的处理结果说明 | | `raw` | 工行原始全字段 | **返回示例(成功)** ```json { "code": "CYT_00000", "data": { "userId": "U123", "result": "成功", "mock": false, "raw": { } } } ``` **异常与处理** | 情形 | 含义与处理 | | --- | --- | | 提示该银行账号存在审批中记录 | 工行按银行卡号去重。同一张卡上一笔还在审批中,等审完再改 | | 提示请确认是否有待审核记录 | 该散户有待审核的记录,先等分行审完 | **前端怎么用** ```js // 直接调,拿 data.result ``` #### ⑥ 散户改卡 / 注销 · 注销 `POST https://openapi.kailingteck.com/api/v1/rinv/seller/close` (数据类) **请求参数** | 字段 | 必填 | 说明 | | --- | --- | --- | | `userId` | 必填 | 散户ID | | `vendorId` | 必填 | 服务商ID | **请求示例** ```json { "userId": "U123", "vendorId": "V1" } ``` **返回参数(data 内)** | 字段 | 说明 | | --- | --- | | `userId` | 回显散户ID | | `result` | 工行给的处理结果说明 | | `raw` | 工行原始全字段 | **返回示例(成功)** ```json { "code": "CYT_00000", "data": { "userId": "U123", "result": "成功", "mock": false, "raw": { } } } ``` **前端怎么用** ```js // 解绑该散户 ``` > 注销后该散户不能再收款开票,如需恢复要重新走②入驻。 --- ### ⑦ 红冲 / 取消 开错了、要撤单时用。红冲=已出票后开一张红票对冲(页面类,要人确认);取消=付款前直接取消这单(数据类),撤单/换单场景用它。红冲后个税、手续费由工行自动冲退,散户不吃亏。 #### ⑦ 红冲 / 取消 · 红冲 `POST https://openapi.kailingteck.com/api/v1/rinv/invoice/red` (页面类) **请求参数** | 字段 | 必填 | 说明 | | --- | --- | --- | | `orderId` | 必填 | 要冲红的原订单号 | | `amount` | 选填 | 冲红金额,不传为全额 | | `projectName` | 选填 | 项目 / 货物名称 | **请求示例** ```json { "orderId": "ORD1", "amount": "100", "projectName": "废纸" } ``` **返回参数(data 内)** | 字段 | 说明 | | --- | --- | | `orderId` | 回显原订单号 | | `pageUrl` | 工行红冲确认页地址,跳过去人工确认 | | `mock` | true=演示数据 | **返回示例(成功)** ```json { "code": "CYT_00000", "data": { "orderId": "ORD1", "pageUrl": "https://openapi.kailingteck.com/api/v1/icbc/redirect/rxxx", "mock": false } } ``` **前端怎么用** ```js location.href = res.data.pageUrl; // 跳工行红冲确认页 ``` > 红冲要开红字确认单,是页面类流程。冲完可再用⑤查结果确认。 #### ⑦ 红冲 / 取消 · 取消开票(推荐) `POST https://openapi.kailingteck.com/api/v1/rinv/invoice/cancel` (数据类) **请求参数** | 字段 | 必填 | 说明 | | --- | --- | --- | | `orderId` | 必填 | 要取消的订单号(与④下单一致) | | `offlineRefund` | 选填 | 是否已线下退款:1=是 / 0=否(默认不上送)。仅特殊场景需要:已支付但缴税失败、或开票失败且线下已把货款退还的单子,传 1 才能取消关闭;付款前的普通撤单不用传 | **请求示例** ```json { "orderId": "ORD1" } ``` **返回参数(data 内)** | 字段 | 说明 | | --- | --- | | `cancelled` | ★true=取消成功(或这单先前已取消过);false=没取消成,看 reason | | `alreadyCancelled` | true=这单先前已经取消过,本次按成功处理——接口幂等,重试安全 | | `reason` | cancelled=false 时,工行给的原始说法 | | `raw` | 工行原始返回(reversal_status:00 取消成功 / 01 取消失败) | **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "data": { "orderId": "ORD1", "cancelled": true, "reason": null, "mock": false } } ``` **前端怎么用** ```js const r = await post('/api/v1/rinv/invoice/cancel', { orderId }); if (!r.data.cancelled) alert(r.data.reason); // 没取消成,按工行的说法处理 ``` > 主场景:预开票成功、散户已确认、但还没付款的订单——撤单/换单时先取消这单,再重新下单。取消成功后用⑤查开票结果确认状态。与「作废」是同一个工行接口(《发票取消接口》),cancel 是推荐入口、返回结构更直白;旧路径 /api/v1/rinv/invoice/void 继续可用,返回结构为旧版(result/status)。 #### ⑦ 红冲 / 取消 · 作废(旧入口) `POST https://openapi.kailingteck.com/api/v1/rinv/invoice/void` (数据类) **请求参数** | 字段 | 必填 | 说明 | | --- | --- | --- | | `orderId` | 必填 | 要作废的原订单号 | | `offlineRefund` | 选填 | 是否已线下退款:1=是 / 0=否(默认)。工行 2026-07 起支持:已支付但缴税失败、或开票失败且线下已完成退款的单子,传 1 才能取消关闭;普通未支付订单不用传 | **请求示例** ```json { "orderId": "ORD1" } ``` **返回参数(data 内)** | 字段 | 说明 | | --- | --- | | `orderId` | 回显原订单号 | | `status` | 工行给的作废状态 | | `result` | 工行给的处理结果说明 | | `raw` | 工行原始全字段 | **返回示例(成功)** ```json { "code": "CYT_00000", "data": { "orderId": "ORD1", "status": "02", "result": "成功", "mock": false, "raw": { } } } ``` **异常与处理** | 情形 | 含义与处理 | | --- | --- | | 提示未查询到订单数据 | 订单号不对,或该单还没到可作废的阶段 | | 已开票的单作废失败 | 票已经开出去了就只能走红冲,不能作废 | **前端怎么用** ```js // 直接作废,拿 data.status / data.result ``` --- ### ⑧ 下载发票 单独下载某张发票文件。日常查结果(⑤)带 withInvoice 就够了,这个用于补下载、单独取票。数据类接口。 #### ⑧ 下载发票 · 下载发票 `POST https://openapi.kailingteck.com/api/v1/rinv/invoice/download` (数据类) **请求参数** | 字段 | 必填 | 说明 | | --- | --- | --- | | `orderId` | 必填 | 订单号 | | `invoiceFormat` | 选填 | 格式:1 ofd / 2 pdf / 3 xml(默认 2) | | `isRedOrblue` | 选填 | 01 蓝票 / 02 红票(默认 01) | **请求示例** ```json { "orderId": "ORD1", "invoiceFormat": "2", "isRedOrblue": "01" } ``` **返回参数(data 内)** | 字段 | 说明 | | --- | --- | | `orderId` | 回显订单号 | | `byteData` | ★票文件的 base64,解一次码就是文件本体(2026-09-16 起网关已把工行返回的双层编码解掉一层;此前自行按文件头做过双层兼容的接入方不受影响) | | `format` | 票文件格式:1 ofd / 2 pdf / 3 xml | | `raw` | 工行原始全字段 | **返回示例(成功)** ```json { "code": "CYT_00000", "data": { "orderId": "ORD1", "byteData": "JVBERi0xLjQK……(base64)", "format": "2", "mock": false, "raw": { } } } ``` **异常与处理** | 情形 | 含义与处理 | | --- | --- | | byteData 为空 | 这一单还没开票成功,先用⑤确认 status=done | | 要红票 | 传 isRedOrblue=02;红冲后才有红票 | **前端怎么用** ```js // data.byteData 是 base64,前端解码成文件下载 const blob = b64toBlob(res.data.byteData, 'application/pdf'); ``` #### ⑧ 下载发票 · 上传重试 `POST https://openapi.kailingteck.com/api/v1/rinv/invoice/uploadRetry` (数据类) **请求参数** | 字段 | 必填 | 说明 | | --- | --- | --- | | `orderId` | 必填 | 上传失败那一单的订单号 | | `userId` | 选填 | 收款散户ID(收方编号)。不传时平台按订单号自动查出来补上,一般不用传 | | `vendorId` | 选填 | 企业子商户编号(付方编号)。不传时同上自动补齐,或取后台配置 | **请求示例** ```json { "orderId": "ORD1" } ``` **返回参数(data 内)** | 字段 | 说明 | | --- | --- | | `accepted` | ★true=重试已受理;false=没受理,看 uploadFailReason | | `uploadStatus` | 工行上传状态:02 已受理 / 05 上传失败 | | `uploadFailReason` | 上传失败的原始原因(如「他用或联用企业开票人未授权开票」) | | `raw` | 工行原始返回 | **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "data": { "orderId": "ORD1", "accepted": true, "uploadStatus": "02", "mock": false } } ``` **前端怎么用** ```js const r = await post('/api/v1/rinv/invoice/uploadRetry', { orderId }); if (!r.data.accepted) alert(r.data.uploadFailReason); // 先修原因再重试 ``` > 仅支持反向蓝票。场景:⑤查结果 status=done_upload_failed(states.upload=05,上传税局失败)时,先把失败原因修掉——最典型的是开票人没在电子税务局做乐企授权,授权补齐后调这里重新上传,不用重新开票;受理后用⑤轮询,upload 变 02、status 变 done 后即可下载票文件。 --- ## 5. 写代码时的硬约束(AI 必读) 1. **页面类接口不要自己做页面**:拿到 `pageUrl` 直接跳转即可。扫码、刷脸、付款都发生在工行的页面上。 2. **不要自己算税**:税额由工行/税局乐企算好,从⑤查开票结果里取回。 3. **不要只看 `code`**:`code=CYT_00000` 只代表调用成功,业务是否通过要看 `data.canInvoice` / `data.status`。 4. **状态是轮询拿的,不会推给你**:③ 的 `canInvoice`、⑤ 的 `status` 都要轮询。建议间隔 3~5 秒,设置最大次数。 5. **`orderId` 全流程唯一**:一单一号,查结果、红冲、作废全靠它,不要复用。 6. **票面备注要主动传**:④开票时传 `notes`(或 `remark`,两个名字都认),内容原样印在发票的「备注」栏,最多 450 字。**不传就是空白**,系统不会自动生成。常写合同号、结算单号、产地。 7. **两个开关按需传,不传就是默认口径**:④开票时 `enterpriseConfirm` 管「谁点确认」(不传/0 散户本人确认,1 企业代确认、跳过散户),`taxPaymentMethod` 管「谁去交税」(不传/1 企业委托扣缴,0 散户自行办理,此时配套传 `iitProject` 与 `supplementaryTax`)。两者独立,不要混成一个开关;缴税方式只有再生资源/报废产品收购场景可用。 8. **付款账户要跟着付款方式传**:④开票时 `asynFlag` 是付款方式(不传/1 企业网银线上异步支付 E企付、0 其他、2 直接支付)。走默认的 `asynFlag=1` 时,工行规格要求同时传 `payerName`(付款账户户名)与 `payerAcctNum`(付款账户账号),否则付款页拿不到扣款账户。**`payerAcctNum` 一定要用字符串**,账号 19~34 位,用数字类型会丢精度。 9. **app_secret 只能放在服务端**,不要写进前端代码或移动端 App。 10. **timestamp 用秒不是毫秒**,差 1000 倍会直接签名失败。 11. **测试环境有时段**:工行测试环境每晚 19:00 后关闭、次日恢复,期间调用返回 502,不是代码问题。 ## 6. 公共错误码 | HTTP / code | 含义与处理 | | --- | --- | | 400 · CYT_40001 | 必填参数没传,`data.hint` 写了缺哪个 | | 401 · CYT_40100 | 缺 X-App-Key 或 X-Sign 请求头 | | 401 · CYT_40102 | 签名校验失败。检查参与签名的 body 是否与实际发送逐字节一致 | | 401 · CYT_40103 | 时间戳过期(±300秒),校准本机时间 | | 403 · CYT_40301 | 该密钥无此产品/渠道权限(工行接口需「反向开票」产品 +「工商银行」渠道) | | 429 · CYT_42901 | 超过每分钟 120 次限流 | | 502 · CYT_50200 | 工行侧暂时不可达(注意测试环境夜间关闭) | | 200 · data.mock=true | 尚未配置工行凭据,返回演示数据、并未真调工行 | --- ## 给 AI 的任务 请用客户指定的语言(未指定则用客户项目的主语言),实现以下内容: 1. 一个 `IcbcClient` 类/模块,封装第 1 节的签名逻辑,`appKey`/`appSecret` 作为配置项传入,**不要硬编码**。 2. 按第 4 节为 8 个接口各写一个方法,参数与返回类型按文档定义。 3. 按第 3 节的顺序写一个完整的业务流程示例:散户入驻 → 查状态 → 开票 → 查结果 → 下载发票。 4. 页面类接口的方法返回 `pageUrl`,并在注释里写明"前端需 location.href 跳转"。 5. 轮询类方法(③⑤)内置重试与超时,不要写死循环。 6. 错误处理按第 6 节:区分"调用失败"(看 code)与"业务未通过"(看 data 里的字段)。 --- # 【乐企联用 · 报废产品收购】 # 乐企联用 · 报废产品收购反向开票 — AI 接入包 > 把本文档整体交给你的 AI(Claude / Cursor / 通义等),说明你的技术栈,它即可写出可直接运行的对接代码。 > 网关:开灵查验通 `https://openapi.kailingteck.com` · 能力:税务总局乐企数字开放平台「报废产品收购开票能力」(编码 202078)。 ## 0. 一句话架构 客户平台只调 **9 个业务接口**(`POST https://openapi.kailingteck.com/api/v1/invoice/reverse/lqbiz/<资源>/<动词>`)。 算税(五税种)、预赋码号段池、自然人自动登记、退税预检全部封装在接口内部;乐企 34 个原子接口另有 1:1 透传通道(`/api/v1/invoice/reverse/lq/<服务编码>`),日常用不到。 演示密钥 `demo_app_key_001` / `demo_secret_001` 走 mock(按官方报文示例返回),可先联调后换正式密钥。 ## 1. 鉴权与签名(所有接口一样) 请求头三件套:`X-App-Key` + `X-Timestamp`(**Unix 秒级**,毫秒必 401)+ `X-Sign` = sha256(appKey + timestamp + 原始请求体 + appSecret) 的 16 进制小写。 > 正式 AppKey / AppSecret 请联系**开灵商务**或您的**开灵项目经理**获取;联调期可先用演示密钥 `demo_app_key_001` / `demo_secret_001`(仅 mock)。 ```js const crypto = require('crypto'); function callLqbiz(action, bodyObj, appKey, appSecret) { const body = JSON.stringify(bodyObj); const ts = String(Math.floor(Date.now() / 1000)); // 必须 Unix 秒级!毫秒会 401 CYT_40103 const sign = crypto.createHash('sha256').update(appKey + ts + body + appSecret).digest('hex'); return fetch('https://openapi.kailingteck.com/api/v1/invoice/reverse/lqbiz/' + action, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-App-Key': appKey, 'X-Timestamp': ts, 'X-Sign': sign }, body, }).then(r => r.json()); } ``` ```python import hashlib, json, time, requests def call_lqbiz(action, body_obj, app_key, app_secret): body = json.dumps(body_obj, ensure_ascii=False, separators=(',', ':')) ts = str(int(time.time())) # 必须 Unix 秒级!毫秒会 401 CYT_40103 sign = hashlib.sha256((app_key + ts + body + app_secret).encode()).hexdigest() r = requests.post(f'https://openapi.kailingteck.com/api/v1/invoice/reverse/lqbiz/{action}', headers={'Content-Type': 'application/json', 'X-App-Key': app_key, 'X-Timestamp': ts, 'X-Sign': sign}, data=body.encode()) return r.json() ``` 响应统一信封:`{ code, msg, requestId, timestamp, data }`,`code === "CYT_00000"` 为成功,业务结论看 `data`。 ## 2. 调用顺序(一单的生命周期) ``` 开票前四道闸:① taxpayer/check → ② agent-tax/check → ③ pending/check → ④ seller/check ↓ 全过 ⑤ tax/calc-order(只传不含税金额 hjje → 返 ykfphm 预开票号码 + dzsphm 电子税票号码 + tax.items 五税种明细) ↓ 银行持 dzsphm 缴税(TIPS 财政缴银) ⑥ tax/pay-status(轮询至 skjnjg=01 已缴纳) ↓ ⑦ invoice/create(fphm = ykfphm → 开出「报废产品收购」蓝字数电票) 售后:⑨ order/cancel 取消未缴款预开单(号码自动回池) / 红冲后 ⑧ refund/apply 一键退税 ``` 关键不变量:实付出售人金额 = 价税合计(jshj) − 应缴税费合计(tax.yjsfhj);④ 内置 60万/180万/500万三道年累计线;⑤ 的税额由网关按官方《算税规则》计算,乐企逐单校验。 ## 3. 接口详情(9 个) ### ① 判断纳税人开票资格 `POST /api/v1/invoice/reverse/lqbiz/taxpayer/check` 收购企业开票资格校验:一次调用同时查纳税人基本信息与风险信息(信用等级/风险类型/预警级别),合并研判后直接给出 eligible 结论。建单前的第一道闸。数据类接口。 - **内置能力**:基本信息与风险信息双查合并研判,一次调用出结论 - **内部编排**:`CXNSRJBXX + CXNSRFXXX` **请求参数** | 参数 | 必填 | 说明 | |---|---|---| | `nsrsbh` | 必填 | 回收企业纳税人识别号/统一社会信用代码(18 位) | **返回参数(data)** | 参数 | 说明 | |---|---| | `eligible` | boolean,是否具备开票资格=基本信息正常 且 未命中风险记录 | | `base.nsrmc` | 纳税人名称 | | `base.djxh` | 登记序号(接口②⑤要用,建议缓存) | | `base.nsrlx` | 纳税人类型:1 一般纳税人/2 小规模/3 转登记小规模/4 辅导期一般人/5 自然人 | | `base.fddbrxm` | 法定代表人姓名 | | `base.scjydz` | 生产经营地址 | | `risk.fxnsrbz` | 风险纳税人标志:Y 风险/N 正常 | | `risk.fxnsrlx` | 风险类型:01 高/02 中/03 低,空=非风险 | | `risk.nsrxydj` | 纳税信用等级:A/B/C/D/M | | `risk.nsryjjb` | 预警级别:01 红/02 黄/03 蓝,空=无预警 | | `hint` | 研判结论(可开票/建议人工复核) | **请求示例** ```json { "nsrsbh": "91371100MA3XXXXX0X" } ``` **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "requestId": "cyt_xxx", "data": { "eligible": true, "base": { "nsrsbh": "91371100MA3XXXXX0X", "nsrmc": "某再生资源回收有限公司", "djxh": "10113700000123456", "nsrlx": "2", "fddbrxm": "王某某", "scjydz": "山东省日照市某区某路 1 号" }, "risk": { "fxnsrbz": "N", "fxnsrlx": "", "nsrxydj": "B", "nsryjjb": "" }, "hint": "基本信息正常且无风险记录,可开票" } } ``` **返回示例(失败/业务拦截)** ```json { "code": "CYT_00000", "msg": "success", "data": { "eligible": false, "risk": { "fxnsrbz": "Y", "fxnsrlx": "02", "nsrxydj": "D", "nsryjjb": "02" }, "hint": "存在风险纳税人信息,建议人工复核后再开票" } } ``` **异常与处理** | 情形 | 含义与处理 | |---|---| | HTTP 502 · CYT_50200 | 基本信息查询失败,data.hint 透传乐企 returnmsg;常见:税号有误、乐企凭证未配置 | | HTTP 200 · eligible=false | 命中风险纳税人:接口本身成功,按 hint 人工复核后决定是否放行开票 | **前端怎么用** ```js const r = await post('/lqbiz/taxpayer/check', { nsrsbh }); if (!r.data.eligible) return alert(r.data.hint); // 风险纳税人 → 人工复核 cache.djxh = r.data.base.djxh; // 登记序号存下来,②⑤要用 ``` > 建议企业开通时调一次并缓存 djxh;风险状态会变,开票高峰期可每日复查。 ### ② 企业代办税费校验 `POST /api/v1/invoice/reverse/lqbiz/agent-tax/check` 查收购企业是否存在「应代办未代办」税费:反向开票后企业须在次月征期内代办申报缴纳,逾期未办会被税局暂停反向开票资格。适合做成每日定时任务,blocked=true 时推送提醒。数据类接口。 - **内置能力**:返回 blocked 结论+逐户明细,可供平台定时轮询提醒 - **内部编排**:`CXYDBWDBSFXX` **请求参数** | 参数 | 必填 | 说明 | |---|---|---| | `dbnsrdjxh` | 必填 | 收购企业登记序号(接口①返回的 base.djxh) | | `dbnsrsbh` | 必填 | 收购企业纳税人识别号 | | `sq` | 必填 | 属期,格式 yyyyMM,如 202607(查该属期的应代办税费) | **返回参数(data)** | 参数 | 说明 | |---|---| | `blocked` | boolean,存在应代办未代办税费时为 true | | `records.ydbsehj` | 应代办税额合计 | | `records.sbqx` | 申报期限 yyyy-MM-dd(超过此日未代办即被阻断开票) | | `records.dbhs` | 应代办自然人户数 | | `records.(明细list)` | 逐条:zrrxm 自然人姓名、zrrsfzjhm 证件号、税额、skssqq/skssqz 税款所属期 | | `hint` | 处理指引 | **请求示例** ```json { "dbnsrdjxh": "10113700000123456", "dbnsrsbh": "91371100MA3XXXXX0X", "sq": "202607" } ``` **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "requestId": "cyt_xxx", "data": { "blocked": false, "records": { "ydbsehj": 0, "dbhs": 0 }, "hint": "无应代办未代办记录" } } ``` **返回示例(失败/业务拦截)** ```json { "code": "CYT_00000", "msg": "success", "data": { "blocked": true, "records": { "ydbsehj": 3560.5, "sbqx": "2026-08-15", "dbhs": 2, "list": [ { "zrrxm": "李建国", "zrrsfzjhm": "3706111979XXXXXXXX", "se": 2165.6 }, { "zrrxm": "赵有才", "zrrsfzjhm": "3706111982XXXXXXXX", "se": 1394.9 } ] }, "hint": "存在应代办未代办税费,请先在电子税务局完成代办申报,否则开票将被阻断" } } ``` **异常与处理** | 情形 | 含义与处理 | |---|---| | HTTP 502 · CYT_50200 | 查询失败;常见:登记序号与税号不匹配、属期格式错误(必须 yyyyMM) | | HTTP 200 · blocked=true | 先在电子税务局完成代办申报缴纳,完成后开票自动恢复;临近 sbqx 应升级提醒 | **前端怎么用** ```js // 每日 09:00 定时任务 const r = await post('/lqbiz/agent-tax/check', { dbnsrdjxh, dbnsrsbh, sq: lastMonth() }); if (r.data.blocked) notifyFinance(r.data.records); // 推送财务:限期内去电子税务局代办申报 ``` ### ③ 企业及自然人未办结业务校验 `POST /api/v1/invoice/reverse/lqbiz/pending/check` 查是否存在「已预开验证税款但未缴税」的挂起交易:有挂单时该自然人不能再开新单,须先缴款或取消预开票。建单前的第三道闸。数据类接口。 - **内置能力**:含未缴税/扣款失败已作废记录识别;命中时需先缴款或调接口⑨取消 - **内部编排**:`CXQYWJSJL` **请求参数** | 参数 | 必填 | 说明 | |---|---|---| | `nsrsbh` | 选填 | 回收企业纳税人识别号(只传税号=查全企业) | | `zrrxm` | 选填 | 自然人姓名(与 sfzjhm 同传=查该自然人) | | `sfzjhm` | 选填 | 自然人身份证件号码;三个参数至少传一项 | **返回参数(data)** | 参数 | 说明 | |---|---| | `blocked` | boolean,存在未办结记录时为 true | | `records.wjskjlsl` | 未缴税款记录数量 | | `records.(逐条)` | ykfphm 预开发票号码、ykfprq 预开日期、ykfpje 金额、ykfpse 税额、jfzt 缴税状态(0 未缴纳/5 扣款失败已作废)、sfjnfs 缴纳方式、lrrq 录入时间 | | `hint` | 处理指引 | **请求示例** ```json { "nsrsbh": "91371100MA3XXXXX0X", "zrrxm": "李建国", "sfzjhm": "37061119790101001X" } ``` **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "requestId": "cyt_xxx", "data": { "blocked": false, "records": { "wjskjlsl": 0 }, "hint": "无未办结业务" } } ``` **返回示例(失败/业务拦截)** ```json { "code": "CYT_00000", "msg": "success", "data": { "blocked": true, "records": { "wjskjlsl": 1, "list": [ { "ykfphm": "24371100000012345678", "ykfprq": "2026-07-18 10:20:00", "ykfpje": 120000, "ykfpse": 1200, "jfzt": "0", "sfjnfs": "0", "lrrq": "2026-07-18 10:20:31" } ] }, "hint": "存在已预开未缴税记录:请先缴款,或调 order/cancel 取消预开票" } } ``` **异常与处理** | 情形 | 含义与处理 | |---|---| | HTTP 200 · blocked=true | 先完成缴款(接口⑥确认 01)或调接口⑨取消该预开票,再建新单 | | HTTP 502 · CYT_50200 | 查询失败,data.hint 透传乐企错误信息 | **前端怎么用** ```js const r = await post('/lqbiz/pending/check', { nsrsbh, zrrxm, sfzjhm }); if (r.data.blocked) { const od = r.data.records.list[0]; // 弹窗二选一:去缴款(带 dzsphm 走缴款流程) / 取消预开(调⑨ order/cancel 带 od.ykfphm) } ``` ### ④ 自然人开票校验 `POST /api/v1/invoice/reverse/lqbiz/seller/check` 出售人(自然人)开票资格校验:查税局阻断名单标志,并返回月累计/年累计开票金额,内置 60 万(个税分档线)/180 万(预警线)/500 万(经营主体登记红线)三道年累计校验。建单前的第四道闸。数据类接口。 - **内置能力**:内置 60 万/180 万/500 万三道年累计校验线 - **内部编排**:`CXZRRKPXX` **请求参数** | 参数 | 必填 | 说明 | |---|---|---| | `zrrxm` | 必填 | 自然人姓名 | | `sfzjhm` | 必填 | 身份证件号码 | | `sfzjlx` | 选填 | 证件类型,默认 201 居民身份证 | **返回参数(data)** | 参数 | 说明 | |---|---| | `allowed` | boolean,=非阻断名单 且 年累计未超 500 万 | | `blockedFlag` | boolean,税局阻断反向开票自然人标志(zdfxkpzrrbz=Y) | | `monthTotal` | 月累计开票总金额(一人式金额,含乐企/电票/税控三渠道) | | `yearTotal` | 年累计开票金额(乐企普票+乐企专票+电票普票) | | `thresholds.over60w` | 超 60 万:个税进入 0.5% 档(60 万内 0.25%,2026 年 12 号公告三流合一口径) | | `thresholds.over180w` | 超 180 万:平台预警线,建议关注 | | `thresholds.over500w` | 超 500 万:红线,不得继续反向开票,应引导办理经营主体登记 | | `detail` | 乐企原始累计字段(lqfxkpppje/lqgsljje/yjgsljse 等,算税引擎同源取数) | | `hint` | 分档结论文案,可直接展示 | **请求示例** ```json { "zrrxm": "李建国", "sfzjhm": "37061119790101001X" } ``` **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "requestId": "cyt_xxx", "data": { "allowed": true, "blockedFlag": false, "monthTotal": 56000, "yearTotal": 420000, "thresholds": { "over60w": false, "over180w": false, "over500w": false }, "detail": { "yljkpzje": 56000, "lqfxkpppje": 420000, "lqgsljje": 300000, "yjgsljse": 750 }, "hint": "校验通过" } } ``` **返回示例(失败/业务拦截)** ```json { "code": "CYT_00000", "msg": "success", "data": { "allowed": false, "blockedFlag": false, "monthTotal": 260000, "yearTotal": 5230000, "thresholds": { "over60w": true, "over180w": true, "over500w": true }, "hint": "年累计开票总额已超 500 万红线,应引导办理经营主体登记" } } ``` **异常与处理** | 情形 | 含义与处理 | |---|---| | HTTP 200 · allowed=false | blockedFlag=true 为税局阻断名单,不可开;over500w=true 引导办理经营主体登记转正向开票 | | HTTP 502 · CYT_50200 | 查询失败;常见:姓名与证件号不一致 | **前端怎么用** ```js const r = await post('/lqbiz/seller/check', { zrrxm, sfzjhm }); if (!r.data.allowed) return alert(r.data.hint); // 阻断/超500万 → 拦截建单 if (r.data.thresholds.over60w) toast('该出售人年累计已超 60 万,本单个税按 0.5% 档'); ``` ### ⑤ 计算代缴税费+下单(核心) `POST /api/v1/invoice/reverse/lqbiz/tax/calc-order` 核心动词:只传不含税金额与票面口径,内置算税引擎按官方《算税规则》算出五税种应缴税额(增值税/城建税/教育费附加/地方教育附加/个人所得税),经乐企校验通过后返回电子税票号码与逐项税额明细。号段取号、自然人未登记自动登记、补缴合并全部内部完成。数据类接口。 - **内置能力**:内置算税引擎+预赋码号段池自动取号+自然人未登记自动登记兜底 - **内部编排**:`CXZRRKPXX → CXFXKPSSCS → 内置算税 → BFCPSGDJSFJY → CXDJSFJKXX(池:QDFPPLFM;兜底:YZYHKHXX+ZRRJJDJ)` **请求参数** | 参数 | 必填 | 说明 | |---|---|---| | `djxh` | 必填 | 回收企业登记序号(接口①返回) | | `nsrsbh` | 必填 | 回收企业纳税人识别号 | | `nsrmc` | 必填 | 回收企业名称 | | `sjswjgdm` | 必填 | 省级税务机关代码(算税参数取数用) | | `zrrxm` | 必填 | 出售人姓名 | | `sfzjhm` | 必填 | 出售人身份证件号码;sfzjlx 默认 201,gjDm 默认 156 | | `hjje` | 必填 | 不含税金额(number)——内置算税引擎的计税基数 | | `ykfplx` | 选填 | 01 增值税专用发票/02 普通发票,默认 02 | | `sfxszy` | 选填 | Y/N 是否销售自用,默认 N;销售自用普票免税,个税按财产转让所得 20% | | `sfxsyhjm` | 选填 | Y/N 专票是否享受 1% 优惠减免,默认 Y;传 N 按 3% | | `bjsfbz` | 选填 | Y/N 补缴税费标志,默认 Y(月累计超 10 万时合并补缴前期免税发票税费) | | `ccyz` | 选填 | 财产原值(number),sfxszy=Y 时用于个税;转让收入不超原值则不缴个税 | | `sfjnfs` | 选填 | 税费缴纳方式:0 自然人自行办理(默认)/1 企业委托扣缴 | | `yhzh / khhszsf` | 选填 | 银行账号/开户行省份;自然人未登记导致预开失败时自动验卡+间接登记后重试 | | `yjsfList` | 选填 | 高级模式:自行算税传入应缴税费list,内置引擎跳过、原样透传乐企校验 | **返回参数(data)** | 参数 | 说明 | |---|---| | `ykfphm` | 预开发票号码(号段池自动取号;开票时即发票号码,接口⑦要用) | | `dzsphm` | 电子税票号码——银行侧缴款(TIPS 财政缴银/三方协议扣款)的唯一凭据 | | `sjse` | 实缴税额(乐企缴款信息返回) | | `tax.cumMonth` | 当前累计金额=一人式月累计+本单金额(10 万起征分界的依据) | | `tax.items[]` | 五税种应缴明细,逐项:zsxmDm/zsxmmc 税种、zspmDm/zspmmc 品目、jsyj 计税依据、slzsl 税率、yjse 应缴税额、yjhkce 已缴或扣除额、jmxzDm 减免性质代码、note 计算说明 | | `tax.yjsfhj` | 应缴税费合计(本单需代缴总额,即从交易款中代扣的钱) | | `tax.face` | 票面口径:hjje 不含税额/zzssl 票面税率/hjse 票面税额/jshj 价税合计/msbz 免税标志 | | `steps[]` | 执行痕迹:累计取数→算税参数→内置算税引擎→待缴税费校验→查询缴款信息,排障与沙箱对账用 | **请求示例** ```json { "djxh": "10113700000123456", "nsrsbh": "91371100MA3XXXXX0X", "nsrmc": "某再生资源回收有限公司", "sjswjgdm": "13700000000", "zrrxm": "李建国", "sfzjhm": "37061119790101001X", "hjje": 120000, "ykfplx": "02", "sfxszy": "N", "bjsfbz": "Y", "sfjnfs": "0", "yhzh": "6222020000012345678", "khhszsf": "370000" } ``` **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "requestId": "cyt_xxx", "data": { "ykfphm": "24371100000012345678", "dzsphm": "37202607000123456789", "sjse": 2165.6, "tax": { "cumMonth": 176000, "yjsfhj": 2165.6, "items": [ { "zsxmDm": "10101", "zsxmmc": "增值税", "zspmDm": "101014001", "zspmmc": "商业", "jsyj": 176000, "slzsl": 0.01, "yjse": 1760, "yjhkce": 0, "jmxzDm": "0001011608", "note": "当张发票+本月待补缴普票合并按 1%" }, { "zsxmDm": "10109", "zsxmmc": "城市维护建设税", "zspmDm": "101090100", "zspmmc": "增值税城市维护建设税", "jsyj": 1760, "slzsl": 0.07, "yjse": 61.6, "yjhkce": 0, "jmxzDm": "0007049901", "note": "按应缴增值税为基数计征,六税两费减征 50%" }, { "zsxmDm": "30203", "zsxmmc": "教育费附加", "zspmDm": "302030100", "zspmmc": "增值税教育费附加", "jsyj": 1760, "slzsl": 0.03, "yjse": 26.4, "yjhkce": 0, "jmxzDm": "0061049901", "note": "六税两费减征 50%,含本月待补缴普票/专票两教" }, { "zsxmDm": "30216", "zsxmmc": "地方教育附加", "zspmDm": "302160100", "zspmmc": "增值税地方教育附加", "jsyj": 1760, "slzsl": 0.02, "yjse": 17.6, "yjhkce": 0, "jmxzDm": "0099049901", "note": "六税两费减征 50%,含本月待补缴普票/专票两教" }, { "zsxmDm": "10106", "zsxmmc": "个人所得税", "zspmDm": "101060200", "zspmmc": "经营所得", "jsyj": 420000, "slzsl": 0.0025, "yjse": 300, "yjhkce": 750, "note": "年度累计不超 60 万,按 0.25% 计征" } ], "face": { "hjje": 120000, "zzssl": 0.01, "hjse": 1200, "jshj": 121200, "msbz": "N", "sfxsyhjm": "" } }, "steps": [ { "step": "自然人累计取数" }, { "step": "算税参数" }, { "step": "内置算税引擎" }, { "step": "待缴税费校验(预开票)" }, { "step": "查询缴款信息" } ], "hint": "税额明细见 tax.items(应缴合计 tax.yjsfhj);持电子税票号码完成缴款(TIPS/三方协议扣款),缴款后调 tax/pay-status 确认,再调 invoice/create 开票" } } ``` **返回示例(失败/业务拦截)** ```json { "code": "CYT_50200", "msg": "查验源暂时不可用,请稍后再试", "requestId": "cyt_xxx", "data": { "hint": "待缴税费校验失败:算税校验不通过", "data": { "steps": [ "…含乐企返回的算税校验结果;沙箱环境含标准答案,用于校准…" ], "tax": { "yjsfhj": 2165.6, "items": [ "…引擎计算结果,便于比对差异…" ] } } } } ``` **异常与处理** | 情形 | 含义与处理 | |---|---| | HTTP 502 · 缺少 hjje | 未传不含税金额且未走高级模式;补传 hjje 即可 | | HTTP 502 · 号段池为空且补领失败 | 预赋码额度用尽:联系平台补充赋额额度后重试 | | HTTP 502 · 待缴税费校验失败(ssjyjg=N) | 算税结果与乐企不一致:沙箱环境 steps 里有标准答案用于校准,生产仅返回算错项;已自动做「自然人间接登记→重试」兜底,仍失败时号码自动回池 | | HTTP 502 · BF9031(查缴款信息) | 单张发票产生多张税票的地区不支持乐企开具:调接口⑨取消交易,改走电子发票服务平台 | **前端怎么用** ```js const r = await post('/lqbiz/tax/calc-order', { djxh, nsrsbh, nsrmc, sjswjgdm, zrrxm, sfzjhm, hjje }); showTaxDetail(r.data.tax.items, r.data.tax.yjsfhj); // 逐项税额展示给操作员确认 order.ykfphm = r.data.ykfphm; // 存:开票用 order.dzsphm = r.data.dzsphm; // 存:交银行缴款用(TIPS) ``` > 实付给出售人的钱=jshj(价税合计)−tax.yjsfhj(代缴税费),资金/发票/税款三流在此对齐。乐企不算税只校验:税额由本接口内置引擎按官方《算税规则》计算(详见主页「内置算税引擎」),每单都过乐企校验闸。 ### ⑥ 查询税费缴纳结果 `POST /api/v1/invoice/reverse/lqbiz/tax/pay-status` 缴款结果确认:银行侧持电子税票号码完成缴税后,以此接口确认「已缴纳」才能进入开票。支持单号与批量。数据类接口。 - **内置能力**:支持单号与批量(dzsphmList) - **内部编排**:`CXSFJNJG` **请求参数** | 参数 | 必填 | 说明 | |---|---|---| | `dzsphm` | 二选一 | 单个电子税票号码(接口⑤返回) | | `dzsphmList` | 二选一 | 电子税票号码数组,批量查询 | **返回参数(data)** | 参数 | 说明 | |---|---| | `dzsphm` | 电子税票号码 | | `skjnjg` | 税款缴纳结果:01 已缴纳/02 未缴纳/03 状态未明/04 税费已作废 | **请求示例** ```json { "dzsphmList": ["37202607000123456789", "37202607000123456790"] } ``` **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "requestId": "cyt_xxx", "data": { "list": [ { "dzsphm": "37202607000123456789", "skjnjg": "01" }, { "dzsphm": "37202607000123456790", "skjnjg": "02" } ] } } ``` **返回示例(失败/业务拦截)** ```json { "code": "CYT_00000", "msg": "success", "data": { "list": [ { "dzsphm": "37202607000123456789", "skjnjg": "04" } ] } } // 04=税费已作废:重新走接口⑤ ``` **异常与处理** | 情形 | 含义与处理 | |---|---| | HTTP 200 · skjnjg=02/03 | 未缴款或状态未明:TIPS 缴款到账有延迟,稍后重查;未确认 01 前不要调接口⑦开票 | | HTTP 200 · skjnjg=04 | 税费已作废(常见于扣款失败):需重调接口⑤重新算税下单 | **前端怎么用** ```js // 缴款发起后轮询(建议 30s 间隔) const r = await post('/lqbiz/tax/pay-status', { dzsphm: order.dzsphm }); if (r.data.skjnjg === '01') await createInvoice(order); // 已缴纳 → 调⑦开票 if (r.data.skjnjg === '04') await recalc(order); // 已作废 → 重走⑤ ``` ### ⑦ 发票开具 `POST /api/v1/invoice/reverse/lqbiz/invoice/create` 缴款确认后开具「报废产品收购」蓝字数电发票:上传票面(发票号码=接口⑤的预开票号码)并自动轮询上传结果至终态。数据类接口。 - **内置能力**:发票上传后自动轮询上传结果至终态;特定要素固定 24-报废产品收购,蓝字标志默认 0 - **内部编排**:`BFCPSGFPSC + CXQDFPSCJG(轮询)` **请求参数** | 参数 | 必填 | 说明 | |---|---|---| | `fphm` | 必填 | 发票号码=接口⑤返回的 ykfphm(20 位) | | `fppz` | 选填 | 发票票种:01 专票/02 普票,默认 02,须与接口⑤ ykfplx 一致 | | `lzfpbz` | 选填 | 蓝字发票标志:0 蓝字(默认)/1 红字 | | `kprq` | 选填 | 开票日期 yyyy-MM-dd HH:mm:ss,默认当天;须与预开为同一月份 | | `kpr / kprzjlx / kprzjhm` | 必填 | 开票人姓名/证件类型(201)/证件号码 | | `qyDm` | 必填 | 区域代码(如 371100 日照) | | `sfxszrrsygdbfcp` | 必填 | Y 销售自己使用过的报废产品/N 销售收购的报废产品(与接口⑤ sfxszy 一致) | | `grsdsxm` | 必填 | 个人所得税项目:1 财产转让所得(销售自用)/2 经营所得 | | `(票面明细行)` | 必填 | 货物名称/规格型号/单位/数量/单价/金额/税率/税额等,字段与原子接口 BFCPSGFPSC 参数表完全一致 | **返回参数(data)** | 参数 | 说明 | |---|---| | `fphm` | 发票号码(=ykfphm) | | `sllsh` | 受理流水号(未终态时凭此续查) | | `result.status` | 上传结果:00 上传成功/01 处理中/02 上传失败/03 发票号码已存在 | | `result.message` | 详细信息(失败原因) | | `hint` | 处理指引 | **请求示例** ```json { "fphm": "24371100000012345678", "fppz": "02", "lzfpbz": "0", "kpr": "张会计", "kprzjlx": "201", "kprzjhm": "370611198501010011", "qyDm": "371100", "sfxszrrsygdbfcp": "N", "grsdsxm": "2", "mxList": [ { "hwmc": "废钢铁", "ggxh": "重废", "dw": "吨", "sl": 4.8, "dj": 25000, "je": 120000, "zzssl": 0.01, "se": 1200 } ] } ``` **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "requestId": "cyt_xxx", "data": { "fphm": "24371100000012345678", "sllsh": "SL20260719000123", "result": { "status": "00", "message": "上传成功" }, "hint": "发票以上传结果为准;未终态可继续用 sllsh 调原子接口 CXQDFPSCJG 查询" } } ``` **返回示例(失败/业务拦截)** ```json { "code": "CYT_50200", "msg": "查验源暂时不可用,请稍后再试", "data": { "hint": "发票上传失败:该预开发票税费未缴纳" } } // 先调⑥确认 skjnjg=01 再开票 ``` **异常与处理** | 情形 | 含义与处理 | |---|---| | HTTP 502 · 上传失败 | 常见:税费未缴清(先用接口⑥确认 01)、fphm 非接口⑤取得、开票月份与预开月份不一致 | | HTTP 200 · result.status=01 | 乐企侧处理中(正常):凭 sllsh 调原子接口 CXQDFPSCJG 续查至终态 | | HTTP 200 · result.status=03 | 该号码已开具过发票:勿重复提交,直接查历史结果 | **前端怎么用** ```js const r = await post('/lqbiz/invoice/create', { fphm: order.ykfphm, kpr, kprzjlx, kprzjhm, qyDm, sfxszrrsygdbfcp: 'N', grsdsxm: '2', mxList }); if (r.data.result.status === '00') markInvoiced(order, r.data.fphm); // 开票完成 else if (r.data.result.status === '01') pollBy(r.data.sllsh); // 处理中续查 ``` > 票面左上角自动标注「报废产品收购」特定要素;开票完成后发票文件经电子税务局/电票平台下载交付。 ### ⑧ 申请反向开票退税 `POST /api/v1/invoice/reverse/lqbiz/refund/apply` 红冲或作废成功后为出售人退税:一次调用办完「可退税额预检 → 提交申请 → 受理结果查询」。无可退税额时直接返回预检结果不误提交。数据类接口。 - **内置能力**:可退税额预检 → 申请 → 受理结果,一次调用办完 - **内部编排**:`CXTSXX → SQFXKPTS → CXBFCPTSJG` **请求参数** | 参数 | 必填 | 说明 | |---|---|---| | `zrrxm` | 必填 | 自然人姓名 | | `sfzjhm` | 必填 | 身份证件号码;sfzjlx 默认 201,gjDm 默认 156 | | `khhszsf` | 必填 | 开户银行所在省份行政区划代码,如 370000 山东省 | | `yhzh` | 必填 | 自然人银行账号(退税入账账户,须与实名一致) | | `tdssqly` | 选填 | 退抵税申请理由 | **返回参数(data)** | 参数 | 说明 | |---|---| | `refundable.ytssehj` | 应退税税额合计(为 0 时无可退明细,不会发起申请) | | `refundable.(明细list)` | 逐条:zsxmDm/zspmDm 税种品目、可退税额、skssswjgDm 税款所属机关 | | `apply.sqjg` | 申请结果:0 成功/1 失败 | | `apply.tdssqxh` | 退抵税申请序号(成功时返回,续查凭据) | | `apply.sbyy` | 失败原因(失败时返回) | | `status.tssqzt` | 退税申请状态:01 审批中/02 审核不同意/03 审核同意/04 申请表作废/05 收入退还书开具中 | | `status.spbtyyy` | 审批不同意原因(tssqzt=02 时返回) | | `hint` | 处理指引 | **请求示例** ```json { "zrrxm": "李建国", "sfzjhm": "37061119790101001X", "khhszsf": "370000", "yhzh": "6222020000012345678", "tdssqly": "发票红冲,申请退还已缴税款" } ``` **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "requestId": "cyt_xxx", "data": { "refundable": { "ytssehj": 1760, "list": [ { "zsxmDm": "10101", "zspmDm": "101014001", "ktse": 1760, "skssswjgDm": "13711000000" } ] }, "apply": { "sqjg": "0", "tdssqxh": "TS2026071900001" }, "status": { "tssqzt": "01" }, "hint": "退税以受理结果为准;未终态可稍后重调本接口查询" } } ``` **返回示例(失败/业务拦截)** ```json { "code": "CYT_00000", "msg": "success", "data": { "refundable": { "ytssehj": 0 }, "hint": "暂无可退税额:请先完成红冲或作废,税款缴纳成功后才会产生可退税额" } } ``` **异常与处理** | 情形 | 含义与处理 | |---|---| | HTTP 200 · refundable.ytssehj=0 | 暂无可退税额:退税前置是红冲/作废成功且税款已缴——先完成红字流程(原子接口红字确认单)再来申请 | | HTTP 502 · 申请失败(sqjg=1) | sbyy 给出原因;常见:银行账号与自然人实名不符、存在在途退税申请 | | HTTP 200 · status.tssqzt=01 | 税务机关审批中(正常):稍后重调本接口续查,重复调用幂等 | **前端怎么用** ```js const r = await post('/lqbiz/refund/apply', { zrrxm, sfzjhm, khhszsf, yhzh }); if (r.data.refundable.ytssehj === 0) return toast(r.data.hint); // 无可退,不误提交 // 有申请序号后轮询终态:03 同意=退款在途;02 不同意=展示 spbtyyy pollUntil(() => post('/lqbiz/refund/apply', sameBody), d => ['02','03','04'].includes(d.status.tssqzt)); ``` ### ⑨ 取消预开票交易 `POST /api/v1/invoice/reverse/lqbiz/order/cancel` 撤销「已算税但未缴款」的预开单:取消成功后预赋码号码自动回池复用。已缴税的交易不能取消,走红冲+退税(接口⑧)。数据类接口。 - **内置能力**:取消成功后预赋码号码自动回池;已缴税/状态不明时号码不回池 - **内部编排**:`QXYKFPJY` **请求参数** | 参数 | 必填 | 说明 | |---|---|---| | `nsrsbh` | 必填 | 回收企业纳税人识别号 | | `zrrxm` | 必填 | 自然人姓名 | | `zrrzjhm` | 必填 | 自然人证件号码(也接受 sfzjhm 字段名) | | `ykfphm` | 必填 | 预开发票号码(接口⑤返回) | | `ykfprq` | 必填 | 预开发票日期 yyyy-MM-dd HH:mm:ss,缺省按当天 | **返回参数(data)** | 参数 | 说明 | |---|---| | `cljg` | 处理结果:00 税费扣款状态不明(稍后重试)/01 取消交易成功/02 税款已缴纳,取消失败 | | `cljgxx` | 处理结果信息(cljg=00 时返回具体说明) | **请求示例** ```json { "nsrsbh": "91371100MA3XXXXX0X", "zrrxm": "李建国", "zrrzjhm": "37061119790101001X", "ykfphm": "24371100000012345678", "ykfprq": "2026-07-19 10:20:00" } ``` **返回示例(成功)** ```json { "code": "CYT_00000", "msg": "success", "requestId": "cyt_xxx", "data": { "cljg": "01" } } ``` **返回示例(失败/业务拦截)** ```json { "code": "CYT_00000", "msg": "success", "data": { "cljg": "02", "cljgxx": "税款已缴纳,取消交易失败" } } // 已缴税 → 走红冲(原子红字四件套)+接口⑧退税 ``` **异常与处理** | 情形 | 含义与处理 | |---|---| | HTTP 200 · cljg=02 | 税款已缴纳不能取消:走红字流程(原子接口红字确认单四件套)开红票,再调接口⑧申请退税 | | HTTP 200 · cljg=00 | 扣款状态不明:等扣款终态后重试;此时号码不回池,避免重复使用 | **前端怎么用** ```js const r = await post('/lqbiz/order/cancel', { nsrsbh, zrrxm, zrrzjhm, ykfphm: order.ykfphm }); switch (r.data.cljg) { case '01': markCancelled(order); break; // 成功,号码已回池 case '02': goRedFlush(order); break; // 已缴税 → 红冲+退税流程 case '00': retryLater(order); break; // 状态不明,稍后重试 } ``` ## 4. 公共错误码(网关层) | HTTP | code | 含义与处理 | |---|---|---| | 200 | CYT_00000 | 成功;业务结论看 data(eligible/blocked/allowed/cljg 等字段) | | 400 | CYT_40001 | 请求体不是合法 JSON,或参数缺失/格式错误 | | 401 | CYT_40100/40101/40102 | 缺少鉴权头/密钥不存在或停用/签名校验失败 | | 401 | CYT_40103 | 时间戳过期或缺失——X-Timestamp 必须为 Unix 秒级(毫秒必错),校准时间后重试 | | 403 | CYT_40301 | 密钥未开通「反向开票」产品权限——请联系开灵商务或您的开灵项目经理开通 | | 404 | CYT_40001(hint:未知业务接口) | 业务接口仅接口清单 9 个;清单外能力走原子通道 /lq/<服务编码> | | 429 | CYT_42901 | 请求过于频繁,稍后重试 | | 502 | CYT_50200 | 上游乐企返回失败:data.hint 为失败原因(透传乐企 returncode/returnmsg),data.data.steps 为已执行步骤 | ## 5. 写代码时的硬约束(AI 必读) - `X-Timestamp` 必须 **Unix 秒**;签名串顺序固定 appKey+ts+body+secret;body 必须与签名用的字符串逐字节一致(先 stringify 再签、再原样发送)。 - ⑤ 返回的 `ykfphm` 与 `dzsphm` 必须持久化:开票(⑦)用 ykfphm,缴款与查缴(⑥)用 dzsphm,取消(⑨)用 ykfphm。 - ⑥ 未确认 `skjnjg=01` 前禁止调 ⑦;⑦ 的 `fppz`、`sfxszrrsygdbfcp`、`grsdsxm` 必须与 ⑤ 的票面口径一致。 - ④ `allowed=false` 与 ①②③ 的 `eligible=false`/`blocked=true` 都要拦截建单,提示语直接用返回的 `hint`。 - 金额单位为元(number,两位小数);`hjje` 是不含税金额,不是价税合计。 - 所有轮询建议 30 秒间隔、指数退避;网关限流 429 时按 Retry 处理。 - 沙箱/演示环境 mock 数据仅供联调,字段结构与生产一致。 --- *开灵科技 · 查验通开放平台 · 本文档由接口定义自动生成,与在线文档同源:https://openapi.kailingteck.com/docs/leqi-scrap*