# 对外 API 接入指南

API 入口为 `https://你的服务域名/api/open/v1`。服务端调用使用 `X-Api-Key`；网页登录 Cookie 不能认证开放接口，API Key 也不能认证网页、管理或内部接口。所有资源属于创建密钥的个人账户，与网页共用订阅、查询次数、查重次数和结果。

完整字段以 [OpenAPI 契约](/openapi-public.json) 为准。部署后的「接入中心 → 接入方式」可以下载 `/openapi-public.json` 和本指南 `/open-api.md`。首期不提供监控、Webhook、人工复核写入和 Excel 导出，也不支持浏览器跨域调用。

## 1. 准备密钥

有效订阅用户（含轻量版）在「接入中心 → API 密钥」（`/integrations/keys`）填写名称并创建密钥。每个账户固定最多 3 个有效密钥，默认有效期 365 天。已撤销密钥不在列表显示；已过期但未撤销的密钥继续显示，已过期或已撤销的密钥均不占有效名额。完整密钥仅在创建时返回一次，请保存到服务端密钥管理器，经环境变量注入；不要置于前端代码、地址栏、日志、版本库或截图中。

有效密钥可调用所属账户的全部开放接口。轮换时先创建新密钥并更新调用方，再撤销旧密钥。撤销后新请求立即失效，已受理任务继续执行。订阅过期或次数耗尽后，已有密钥仍可读取已生成的结果；订阅过期禁止创建新任务及密钥。成功请求可以使用原幂等键重放。创建响应丢失时先在列表中撤销无法取得完整密钥的项目，再重新创建。

开放接口随服务启动即可使用，所有请求仍须通过 API Key 鉴权和资源归属校验。创建密钥及新任务需要有效订阅（含轻量版）；查重任务另需专业版或企业版查重权益；已有密钥的查看和撤销不受套餐限制。

## 2. 接口与请求约定

所有路径均相对于 `/api/open/v1`。JSON 使用 UTF-8，请求加 `Content-Type: application/json`。创建任务必须携带非空、最长 128 字符的 `Idempotency-Key`，建议使用 UUID；服务端按实际任务计算并预留次数。请求体不接受未知字段、空必填项或类型隐式转换，数组至少一项；准确的必填、枚举、长度及分页边界以 OpenAPI schema 为准。

| 方法与路径 | 作用 |
| --- | --- |
| `GET /account/entitlements` | 订阅状态、API 新任务资格、查询及查重可用次数 |
| `POST /queries` | 整批受理 1–100 个主体，返回 `202` |
| `GET /queries`、`GET /queries/{id}` | 近 6 个月查询列表；按 ID 读取进度与摘要不受时间限制 |
| `GET /queries/{id}/registrations` | 分页登记摘要，支持多状态筛选，默认包含实际附件元数据与下载链接 |
| `GET /queries/{id}/registrations/{registrationId}` | 完整结构化证明与附件元数据 |
| `POST /checks` | 创建自动查重任务，返回 `202` |
| `GET /checks`、`GET /checks/{id}` | 查重列表、执行状态与完整性 |
| `GET /checks/{id}/results`、`GET /checks/{id}/results/{registrationId}` | 候选登记、命中条件、系统判断及人工结论 |
| `GET /checks/{id}/results/{registrationId}/navigation` | 按与列表相同的筛选和排序读取位置、前后登记，不提供分页参数 |
| `GET /checks/{id}/results/{registrationId}/evidence-locations` | 按 `documentId` 和 `pageNumber` 读取定位信息 |
| `GET /documents/{id}/content` | 流式下载证明或附件，支持单段 `Range` |

分页采用 `?page=1&pageSize=20`，页码从 1 开始，每页 1–100 条；超出范围返回 `400`，不自动截断。响应为 `{items,total,page,pageSize}`。查询和查重列表支持 `search`、`status`，登记列表支持 `search`、多选 `parseStatus`；具体过滤项见契约。空 `status` 表示全部，非空值必须属于对应接口枚举，拼写错误返回 `400`。搜索中的 `%`、`_`、`!` 按字面匹配，不能作为通配符。所有资源和嵌套资源都校验本人归属，不存在或无权访问返回 `404`。文件 URL 同样需要 `X-Api-Key`，不能直接用浏览器链接代替服务端下载。任务报价接口 `/billing/quotes` 不再开放；创建任务直接按服务端规则受理计次。

网页和开放 API 的查询历史列表统一展示近 6 个自然月：服务端以北京时间当天零点向前推 6 个月，按查询 `createdAt` 筛选并包含边界当天。例如 2026-10-11 的起点为 2026-04-11 00:00（北京时间）。搜索、状态、总数以及网页批次和全选均使用此范围，不提供扩大范围的参数。历史数据及文件不会删除，已有查询 ID 的详情、登记清单和下载继续按归属权限访问，关联查重、监控及计费记录不受影响。

### 账户资格与次数

`GET /account/entitlements` 的 `active` 表示是否有有效订阅，`apiEnabled` 表示当前订阅是否允许通过 API 创建新任务。`apiEnabled=true` 不表示剩余次数充足或具有查重权益；分别读取 `query.available` 和 `check.available`，最终以创建响应为准。订阅过期后仍可读取已有结果、重放原成功请求，客户端不能用 `apiEnabled=false` 阻止原请求重放。

`query`、`check` 各自的 `total`、`used`、`reserved`、`retained` 汇总当前有效额度桶，排除已过期和未来生效额度；其中 `used` 为这些有效桶中已结算的次数，`reserved` 为已预留未结算次数，`retained` 为账面未用且未预留次数。`available` 是当前可以使用的次数；订阅无效时可能为零，即使仍有 `retained`。这些值不是账户历史累计消费报表。

### 查询：提交、轮询、读取

直接提交 `POST /queries`：

```json
{"subjectNames":["示例融资企业有限公司"]}
```

响应形状为 `{"queries":[{"id":"…","status":"queued","executionStatus":"queued",…}]}`。企业全称或18位身份证号码规范化去重，整批受理或整批回滚。每个查询 ID 单独轮询 `GET /queries/{id}`，完成后分页读取登记清单和附件。需要重新获取上游资料时，以新的幂等键重新创建查询并正常计次。

个人查询沿用 `subjectNames` 字符串数组，查重沿用 `applicant`，不新增主体类型字段；返回的 `subjectName`／`applicant` 是规范化后的主体。服务端自动识别18位大陆居民身份证，统一全角、空白和末位 X 大小写，校验出生日期与校验位；证件候选不合法时整批拒绝，不预留次数、不回退为企业查询。首版不支持15位身份证及护照等其他个人证件。企业与个人可混合批量提交；身份证仅确定查询主体，查重仍须提供发票号或关键词。

### 查重：新查询或复用资料

提交 `POST /checks`：

```json
{
  "applicant":"示例融资企业有限公司",
  "criteria":[{"kind":"invoice_no","value":"12345678901234567890"},{"kind":"keyword","value":"合同编号示例"}]
}
```

条件为 1–100 个，类型为 `invoice_no` 或 `keyword`，每项 `value` 为非空字符串、最长 200 字符；查询主体为企业全称或18位大陆居民身份证号码，最长 100 字符。场景固定为 `asset_check`，单次查重只接受一个主体。

复用资料时，在提交中加入 `sourceRegistryQueryId`。来源必须是本人、同一主体且已结束的成功或部分完成查询；`applicant` 仍必填。

创建查重返回单个任务对象，使用 `id` 轮询 `GET /checks/{id}`。执行完成后读取候选列表；候选的 `evidence` 提供文档、页码、命中片段，`evidence-locations?documentId=…&pageNumber=1` 提供页内坐标。文档下载地址都位于开放接口下。

## 3. 状态、计次与重试

**以 `executionStatus` 判断后台执行是否结束。** `queued`、`waiting_resource`、`processing` 需要继续轮询；`succeeded`、`partial`、`failed`、`cancelled` 是执行终态，`cancelled` 表示查询已取消。字段缺失或出现未知状态时应报错并保留任务 ID，不能按成功或完成处理。建议间隔从 3 秒逐步退避至 30 秒，并设置调用方总时限。长期等待资源可稍后继续轮询已有 ID，不要重复创建任务。

任务 `status` 表达业务状态，例如查询明确零登记为 `no_result`，查重执行结束后可能为 `review_pending`。候选的 `systemDecision` 与 `manualReview` 分别返回系统判断与已有人工结论；人工判断 `confirmed` 为确认风险，`false_positive` 为误报，任务的 `confirmedRiskCount`、`excludedCount` 仅统计人工结论。API 首期不写人工结论。

始终同时检查 `coverageComplete` 和 `sourceStatus`：`coverageComplete=false` 或 `sourceStatus` 为 `quarantined`、`unavailable` 时，未命中不能解释为排除风险。部分解析失败的结果仍可能包含有用命中。隔离资料不能下载；明确零登记是正常的已完成查询。

查询和查重任务以可选 `failure: {code,message}` 返回受控失败或覆盖不足原因，不返回上游错误原文。`code` 为 `SOURCE_QUARANTINED`、`SOURCE_UNAVAILABLE`、`QUERY_FAILED`、`CHECK_FAILED`、`DOCUMENT_COVERAGE_INCOMPLETE` 或 `CHECK_COVERAGE_INCOMPLETE`；结合状态和完整性判断，`failure` 存在不代表已有结果全部无用。公开查重任务不再返回 `processingStartedAt`，不要把创建时间当成实际工作开始时间；使用 `createdAt`、`processingCompletedAt`、`completedAt` 及状态描述当前进度。

网页的登记证明结果接口 `GET /api/v1/checks/{id}/results` 使用 `resultStatus=all|pending|confirmed|excluded|unmatched` 筛选，返回四类互斥的 `counts`，以及每份证明的 `resultStatus`、待处理原因和资料覆盖原因。`pending` 同时包含待人工复核的命中证明与资料尚不足以判定的证明；后者不能提交人工结论。原有系统匹配与人工复核字段继续单独记录。

| 操作 | 查询次数 | 查重次数 |
| --- | --- | --- |
| 查询每个去重后的主体 | 1 | 0 |
| 新资料查重 | 1 | 1 |
| 复用本人已有资料查重 | 0 | 1 |

新任务按现有计费规则预留；查询取得完整登记清单后结算，包含明确零登记。后续下载或解析失败不退查询次数。网页和 API 并发使用同一额度；以服务端最终受理结果为准。

为每次新的业务意图生成唯一 `Idempotency-Key`（建议 UUID），网络超时或连接中断时使用**相同路径、请求体和幂等键**重试。成功重放返回首次受理响应，不代表最新进度；最新状态通过 GET 读取。同一用户的不同密钥共享幂等空间。成功重放先通过认证校验，再使用已存响应，因此订阅过期、次数不足不会导致重试再次计次。相同幂等键搭配不同请求体返回冲突。

读取账户权益不预留次数；即使读取时次数充足，提交时也可能因并发消耗而返回次数不足。以服务端受理结果为准。

默认同一用户所有密钥合计：创建任务 10 次/分钟，其他请求 120 次/分钟，文件下载同时最多 3 个连接。`429` 携带 `Retry-After`；等待后继续相同意图。Redis 不可用返回受控 `503`，不进入收费执行。这些参数用于接口保护，不代表上游吞吐承诺。

响应均有 `X-Request-Id`。错误 JSON 为 `{"code":"机器错误码","message":"公开消息","requestId":"请求编号"}`。字段校验失败时可以附带 `details.fields: [{path,code,message}]`，`path` 如 `criteria[3].value`，用于定位错误项；只返回字段路径和受控规则说明，不回显客户原文或密钥。缺少 `details` 时仍按顶层错误码处理。保留请求编号用于排查，不记录密钥、请求正文或客户原文。常见状态：

| HTTP | 处理方式 |
| --- | --- |
| `400` | 检查请求字段、分页与条件格式 |
| `401` | 检查密钥是否有效、过期或已撤销 |
| `402` | `SUBSCRIPTION_REQUIRED` 需要有效订阅；`QUERY_ALLOWANCE_INSUFFICIENT` 或 `CHECK_ALLOWANCE_INSUFFICIENT` 需要补充对应次数 |
| `403` | `CHECK_PLAN_REQUIRED` 需要查重权益；`ACCOUNT_DISABLED` 表示账户已停用 |
| `404` | 核对本人资源 ID 和嵌套归属 |
| `409` | 检查错误码；幂等冲突不能更换正文强行重试 |
| `429` | 按 `Retry-After` 退避 |
| `503` | 服务暂不可用；稍后重试已有请求，避免生成新的业务意图 |

文件下载成功返回 `200`，合法单段范围返回 `206`，不满足范围返回 `416`。下载完整文件时不加 `Range`，需要部分内容时例如 `Range: bytes=0-1023`；多段范围不受支持。

## 4. 可运行示例

以下示例源码位于代码仓库的 `docs/examples/open-api`，部署后的指南也提供各文件下载。从密钥管理器注入 `ZDW_API_KEY`，设置 `ZDW_API_BASE=https://你的服务域名/api/open/v1`；示例仅允许 HTTPS，或本机开发的 HTTP 地址。命令行中的企业名称、条件应在实际系统中由应用传入，不要将客户原文纳入命令日志。

- [curl.sh](examples/open-api/curl.sh)：需要 Bash、curl、jq 和 Python 3（生成 UUID）。`bash docs/examples/open-api/curl.sh query '企业全称' --idempotency-key UUID`；`check '企业全称' '关键词' [来源查询ID] --idempotency-key UUID`；`download '文档ID' '本地输出文件'`。
- [client.py](examples/open-api/client.py)：Python 3 标准库。`python3 docs/examples/open-api/client.py query '企业全称' --idempotency-key UUID`；`check '企业全称' '关键词' --source-query '来源查询ID' --idempotency-key UUID`；去掉 `--source-query` 为新资料查重；`download '文档ID' '本地输出文件'`。
- JDK 17、Maven：下载 [pom.xml](examples/open-api/java/pom.xml) 和 [OpenApiClient.java](examples/open-api/java/src/main/java/OpenApiClient.java)，保持 `pom.xml` 与 `src/main/java/OpenApiClient.java` 的目录关系。使用 JDK 17 在仓库中执行 `mvn -f docs/examples/open-api/java/pom.xml compile exec:java -Dexec.args="query 企业全称 --idempotency-key UUID"`。查重参数为 `check 企业全称 关键词 [来源查询ID] --idempotency-key UUID`，下载为 `download 文档ID 本地输出文件`。

三个示例会直接提交并轮询至明确执行终态；遇到未知状态会报错，不会将结果直接判定为无风险。示例仅输出任务编号、状态和完整性，不打印密钥或客户原文。下载不会覆盖已有文件。`UUID` 须替换为本次业务意图的真实唯一键；省略 `--idempotency-key` 时每次运行都会生成新键并打印到 stderr，因此恢复旧请求必须显式传入原键，同时逐项保留原企业名称、条件和来源 ID，不能更改正文。生产系统应在首次发送前将幂等键与原业务请求可靠保存；已取得任务 ID 时直接 GET 续查即可。查询与查重每个新意图可能正常计次，请使用本人授权的资料。

## 5. 配置与验收

开放接口无需单独启用。`OPEN_API_CREATE_PER_MINUTE`、`OPEN_API_READ_PER_MINUTE`、`OPEN_API_DOWNLOAD_CONCURRENCY` 分别配置创建、读取和并发下载限制。每个账户最多 3 个有效密钥，上限不可配置；`OPEN_API_KEY_LIFETIME_DAYS`、`OPEN_API_LOG_RETENTION_DAYS` 分别配置密钥有效期与调用记录保留期限，默认分别为 365、30 天。密钥管理保留会话和 CSRF 校验，不接受 API Key 自助管理密钥。调用记录只保存请求编号、密钥 ID、路由模板、状态、错误码与耗时。

开发验收需验证真实 PostgreSQL 的额度竞争、批量回滚和幂等，真实 Redis 的聚合限流、下载并发与故障，以及浏览器中的一次展示、撤销和记录分页；数据库仅用 `zdw_test` 随机 schema。真实上游、GPU、容量与 24 小时观察属于独立验收，不能用示例或模拟数据冒充。

### 查重结果总览与连续查看

`GET /api/v1/checks/{id}/results` 返回分页的 `CheckResultSummary`，包含共用 `registration: RegistrationSummary`、匹配和结果状态、命中条件 ID、原因及复核说明，不包含附件清单、原文或证据；共用摘要保留原件链接。登记字段直接取自该登记的已解析证明，缺失时不推算或补造。`counts` 是全任务四类数量，`total` 是当前筛选数量，`reviewableCount` 是全任务中来源可用且尚未判断的候选数；不会随着列表筛选缩小。搜索覆盖证明编号、全部主体名称、已命中条件的值，大小写不敏感，`%`、`_`、`!` 按字面匹配。

网页与开放 API 的 `GET /checks/{id}/results/{registrationId}` 都以 `registration` 返回完整登记，以 `check` 返回证据、匹配及人工判断，不复制证明字段，不公开候选 ID。开放 API 使用独立响应模型：保留业务证明、PDF 下载元数据、系统证据和已有人工结论，去除 PDF 内部错误／解析版本、证据内部任务 ID、复核人及复核历史。公开证据的 `matchedAt` 表示本次匹配产生时间，替代原先含义不准确的 `indexedAt`；`indexVersion` 表示匹配使用的检索版本，不能把 `matchedAt` 解释为文档建索引时间。

公开 `GET /api/open/v1/checks/{id}/results/{registrationId}/navigation` 接受与列表相同的 `search`、`resultStatus`、`matchStatus`、`criterionId`、`strength`、`reviewableOnly`、`sort`，不提供 `page`、`pageSize` 参数。按相同排序返回 `total`、从 1 开始的 `position`、`previousRegistrationId`、`nextRegistrationId`；导航范围始终是完整筛选集合。合法登记不在筛选内时位置和相邻 ID 为 null；越权或不属于该任务的登记返回 404。结果不再返回全局候选位置 `candidatePosition`。

统一结果目录 `GET /api/v1/checks/{id}/results` 支持 `reviewableOnly=true`，限定为来源可用且未判断的候选。总览的“开始复核”使用该参数进入全任务队列，结果行则保留当前筛选并逐份浏览全部登记。导出仍读取完整登记结果，四类状态与页面一致。

### 查询与查重的文档覆盖

查询下载主证明与全部附件，只解析主证明原生文本，不调用 OCR。查询 coverageComplete 表示资料取得与主证明解析完整；附件 status=not_requested 表示原件可访问、尚未发起识别。登记 indexStatus=not_requested 表示未准备查重索引。

查重受理后才准备附件文本和扫描识别，处理期间保持 processing。查重 coverageComplete 独立表示证据检索是否完整，不能从来源查询的同名字段推断。附件识别失败不改变来源查询状态；识别补齐可使原先主证明解析不完整的查询产生完整查重结果。

## 查询与证明读取契约

网页 `GET /api/v1/queries/{id}/registrations` 与开放接口 `GET /api/open/v1/queries/{id}/registrations` 返回分页摘要。每项包含 `id`、`certificateNumber`、`businessTypeCode`、`subBusinessTypeCode`、`registerType`、`registerTime`、`registrationMonths`、`expiresAt`、`debtorNames`、`securedPartyNames`、`propertySummary`、`pdfUrl`、`parseStatus`、`sourceStatus`、`coverageComplete`、`dataSource` 和独立的 `upstreamSummary`。可空字段为 null 时省略。`dataSource` 标明摘要来自解析证明还是上游概要，不能代表解析成功。

`propertySummary` 最多 200 个 Unicode 字符，搜索仍覆盖完整财产描述、主体名称和合同编号。`parseStatus` 为 `completed|pending|failed|missing`，来源不可用时不返回 `pdfUrl`，开放接口链接使用 API Key 访问。详情中的 `certificate` 可以为空；此时只展示 `upstreamSummary` 中实际存在的概要，不伪造财产、主体或合同。

两个登记清单接口都支持多选 `parseStatus`。以下写法等价，表示主证明解析完成或失败的登记：

```http
GET /api/open/v1/queries/{id}/registrations?parseStatus=completed&parseStatus=failed&page=1&pageSize=20
GET /api/open/v1/queries/{id}/registrations?parseStatus=completed,failed&page=1&pageSize=20
```

各状态取并集，再与 `search` 条件取交集；`total` 为筛选后的登记数，超出最后一页返回空 `items` 并保留总数。省略、空值或单独 `all` 表示全部；首尾空白、空项及重复值会被忽略。未知状态、`all` 与具体状态混用返回 `400`，`details.fields[].path` 为 `parseStatus`。未取得主证明优先归为 `missing`；有主证明但资料被隔离归为 `failed`。该状态不代表附件识别或整体覆盖状态。

开放接口的每条清单摘要默认包含 `attachments`，没有实际附件时为 `[]`，无需额外参数。每项沿用详情的 `id`、`fileName`、`url`、`mimeType`、`status`、`sha256`、`pageCount`，可选字段沿用详情规则，按文件名和文档 ID 排序。仅包含实际取得的附件，不把证明中的附件声明当成可下载文件，也不内嵌文件或全文。`url` 为 `/api/open/v1/documents/{id}/content`，下载时须携带 `X-Api-Key`；来源不可用或隔离时下载返回 `409`。附件 `status=not_requested` 表示尚未发起识别，不影响主证明的 `parseStatus`。网页清单和查重摘要仍不包含附件清单。

完整证明采用 v2 业务模型：官方大类／小类代码、五种登记类型，主体角色为 `debtor|secured_party`。名称只保存在 `parties` 中，摘要名称由主体集合生成。主合同金额、财产价值和最高债权额分别保存币种与十进制字符串；日期为 ISO 日期，登记时间带时区。填表人、原登记说明与登记专属内容分别为 `preparer`、`originalRegistration`、`registrationDetails`。异议陈述不能作为财产描述。

完整财产描述、当事人、合同和登记链通过单份详情读取；实际附件元数据可通过开放接口清单或详情读取。Excel 导出使用完整财产描述，不受列表摘要长度及筛选条件影响。实际附件名称取 `attachments[].fileName`，原文声明为 `certificate.declaredAttachments[]`，包含 `fileName` 和 `purpose`（`property|objection|property_elements`）。XLSX 原件保留下载，但本版不解析内容，因此相关查重必须提示覆盖不完整。

详情页直接展示非空业务字段。主体显示称呼由官方业务代码决定，例如出质人／质权人、承租人／出租人、买受人／出卖人；Excel 分别列出角色与名称。旧 `transferor/assignee`、`businessType`、`timeLimit` 和混用的 `securedContractAmount` 不提供兼容响应。

证明以登记 `id` 和 `pdf.documentId` 标识；登记链仅展示同 owner、同 query 的关联证明，不推断法律效力。执行状态、资料完整性、计次和管理员恢复字段保留原有含义；查重证据的真实 `indexVersion` 独立于解析版本。

### 统一证明工作区

网页与公开 API 只保留 `/checks/{id}/results`，详情、导航和证据位置均用 `registrationId`。`matchStatus=matched&resultStatus=pending` 表示未人工判断的系统命中；`reviewableOnly=true` 进一步限定来源可用。`sort=resultStatus|reviewPriority` 控制结果分组顺序或待复核优先顺序，列表与导航使用相同选择。网页通过 `POST /api/v1/checks/{id}/results/{registrationId}/reviews` 提交 `{decision,note,version}` 与幂等键；公开 API 不开放人工复核写入。
