API 开放平台 · 开发者文档测试环境(沙箱)客户控制台

概述

本平台在企享云发票能力之上提供统一的自有 API:你只需要使用在本平台控制台创建的 API Key,不需要、也不会接触任何企享云账号或签名。平台负责上游鉴权、签名、计量、幂等和结果缓存。

当前开放的是测试环境(沙箱):发票接口连接平台内置的企享云模拟器,返回合成数据,可用于完成对接开发;额度单位为测试单位 TEST_UNIT,不产生费用。服务地址:http://127.0.0.1:3107。

快速开始

  1. 在 客户控制台 注册企业账号,状态为“待审核”。
  2. 平台审核激活后,为你开通接口授权和测试额度。
  3. 在控制台“应用与 Key”中创建应用,勾选需要的接口,然后创建 API Key。完整 Key 只显示一次,请立即保存。
  4. 按下方示例调用接口。示例从环境变量读取 Key,避免把 Key 写进命令行历史或代码。

鉴权

所有客户接口使用 HTTP Bearer:Authorization: Bearer ak_test_<id>.<secret>。Key 绑定租户、应用和 scope,每次调用都会实时校验 Key、应用、租户和接口授权;停用或吊销后立即失效(401)。Key 只能在服务端使用,不要放在网页前端、移动端或公开仓库中。泄露时请在控制台吊销或轮换。

幂等与重试

所有写接口必须带 Idempotency-Key(1–128 位字母、数字或 ._~-)。同一个 Key 与相同请求体重放会返回原结果,不会重复受理或重复扣额度;同一个 Key 配不同请求体返回 409 IDEMPOTENCY_CONFLICT。

网络超时或收到 503 且 outcome 为 UNKNOWN 时,请使用原 Idempotency-Key 和原请求体重试,不要换新 Key,否则可能产生第二笔业务。

异步任务与结果未知

发票查验与版式任务提交均为批量异步受理:返回 202 和 batchId,每张发票是一个子项(requestId),独立处理、独立计量。用 GET 接口轮询本地结果,轮询不消耗额度,也不会再次调用上游。

  • SUCCEEDED / FAILED_CONFIRMED:最终结果,已结算。
  • RESERVED / DISPATCH_INTENT / PENDING:处理中,额度保持预占。版式任务受理后为 PENDING,需要调用“提取版式结果”。
  • UNKNOWN:平台无法确认上游是否已执行。额度保持预占,平台会对账,不会自动重发;请勿自行用新的 Idempotency-Key 重提。

版式结果提取是明确的消费动作,每个任务最多 3 次;完成后可通过任务查询中的 fileUrl 下载文件(需当前应用的有效 Key)。

额度与限速

每张发票预占 1 个测试单位:最终成功扣减,确认失败按平台策略扣减或释放,未执行释放。额度不足时整批拒绝(429 QUOTA_EXHAUSTED),不会部分扣减。超过速率或在途并发上限时返回 429 并带 Retry-After(秒)。额度、用量和账本可在控制台查看。

调用示例

先在终端设置服务地址,并以不回显的方式输入 Key:

export API_BASE=http://127.0.0.1:3107
read -rs API_KEY   # 粘贴 Key 后回车;不显示、不进入命令历史
export API_KEY

curl:提交发票查验

curl -sS -X POST "$API_BASE/v1/mock/qxy/verifications" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"items":[{"itemId":1,"invoiceNumber":"99990000000000000001","issueDate":"2026-02-28","totalCents":12345}]}'

Node.js 24:查验并等待结果

保存为 verify.mjs,执行 node verify.mjs:

const base = process.env.API_BASE, key = process.env.API_KEY;
const headers = { authorization: `Bearer ${key}`, 'content-type': 'application/json' };
const items = [{ itemId: 1, invoiceNumber: '99990000000000000001', issueDate: '2026-02-28', totalCents: 12345 }];
const admitted = await fetch(`${base}/v1/mock/qxy/verifications`, {
  method: 'POST', headers: { ...headers, 'idempotency-key': crypto.randomUUID() }, body: JSON.stringify({ items }),
});
let batch = await admitted.json();
if (!admitted.ok) throw new Error(`${admitted.status} ${batch.code}`);
for (let i = 0; i < 60 && !batch.terminal; i++) {
  await new Promise(resolve => setTimeout(resolve, 500));
  batch = await (await fetch(`${base}${batch.statusUrl}`, { headers })).json();
}
console.log(batch.outcome, batch.items.map(item => `${item.itemId}:${item.outcome}`).join(' '));

Node.js 24:版式任务提交、提取结果并下载

const base = process.env.API_BASE, key = process.env.API_KEY;
const headers = { authorization: `Bearer ${key}`, 'content-type': 'application/json' };
const get = async path => (await fetch(`${base}${path}`, { headers })).json();
const wait = () => new Promise(resolve => setTimeout(resolve, 500));
const items = [{ itemId: 1, invoiceNumber: '99990000000000000002', issueDate: '2026-02-28', totalCents: 12345 }];
const submitted = await fetch(`${base}/v1/mock/qxy/format-tasks`, {
  method: 'POST', headers: { ...headers, 'idempotency-key': crypto.randomUUID() }, body: JSON.stringify({ items }),
});
let batch = await submitted.json();
if (!submitted.ok) throw new Error(`${submitted.status} ${batch.code}`);
const taskId = batch.items[0].requestId;
let task = await get(`/v1/mock/qxy/tasks/${taskId}`);
for (let i = 0; i < 60 && task.state !== 'PENDING'; i++) { await wait(); task = await get(`/v1/mock/qxy/tasks/${taskId}`); }
const fetchKey = crypto.randomUUID();
const permit = await fetch(`${base}/v1/mock/qxy/tasks/${taskId}/result-fetch`, {
  method: 'POST', headers: { ...headers, 'idempotency-key': fetchKey }, body: '{}',
});
if (!permit.ok) throw new Error(`${permit.status} ${(await permit.json()).code}`);
for (let i = 0; i < 60 && task.state === 'PENDING'; i++) { await wait(); task = await get(`/v1/mock/qxy/tasks/${taskId}`); }
console.log(task.state, `remaining=${task.extraction.remaining}`);
if (task.fileUrl) console.log((await (await fetch(`${base}${task.fileUrl}`, { headers })).text()).trim());

错误码

错误响应统一为 {"requestId","code","message","retryable","outcome"}。outcome=NOT_DISPATCHED 表示未调用上游;UNKNOWN 表示需要按原幂等键重试或等待对账。

加载中…

接口参考

以下内容由服务端实际注册的接口自动生成,机器可读版本:/docs/openapi.json。

加载中…