通用约定
通用约定
适用于每个端点的规则,在此统一说明一次,而不是在二十五处重复。
幂等性
会产生变更的端点接受 Idempotency-Key 请求头。用同一个键重复请求,会重放原始响应而不是再执行一次,因此超时后的重试不会创建出第二份护照。键归属于单个组织,两个客户永远不会因同一取值而冲突。测试与正式相互独立:已在一种模式下用过的键,在另一种模式下会被拒绝而不是重放。 有一个例外,它属于一种能力而非事实:`POST /documents` 返回的带签名上传 URL 在文件尚未送达时会于重放中重新签发,文件送达后则不再返回——重放一个已过期的 URL 等于忠实地返回了一个无法使用的东西。
一个键会被记住 30 天。此后它被彻底遗忘,携带它的重试将重新执行而不是重放,因此可能延后运行的对账任务应当改用新的键。
curl "https://passportcraft.com/api/v1/organizations/{organization}/passports" \
-X POST \
-H "Authorization: Bearer $PASSPORTCRAFT_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"category":"textile"}'分页
集合一律采用游标分页,绝不使用偏移量。在并发写入的情况下偏移量本身就是错的:两次取页之间插入的一条记录会让其后所有记录整体移位,客户端会在毫无察觉的情况下漏读或重复读取。
把 next_cursor 作为 cursor 回传即可取得下一页。游标会编码签发时所用的筛选条件,因此改用其他条件发送会被拒绝,而不是返回一页悄悄换了含义的数据。
{
"object": "list",
"data": [],
"has_more": true,
"next_cursor": "eyJjIjoiMjAyNi0wOC0wMVQxNDowMzoyMi41MDFaIn0"
}条件写入
读取护照时会返回携带版本号的 ETag。在 PATCH 请求中以 If-Match 回传,若护照在此期间已被改动,写入就会被拒绝,这是让读取-修改-写入在并发编辑面前保持安全的唯一办法。
curl "https://passportcraft.com/api/v1/organizations/{organization}/passports/{id}" \
-X PATCH \
-H "Authorization: Bearer $PASSPORTCRAFT_API_KEY" \
-H "If-Match: \"4\"" \
-H "Content-Type: application/json" \
-d '{"data":{"recycled_content_percentage":38}}'速率限制
每个携带有效密钥的请求都有三个计数器同时生效,以最严格的那个为准。公布的数值是刻意保守的起点,而非调优后的上限。
| 计数器 | 请求数 | 时间窗 |
|---|---|---|
| key_org | 1000 | 60s |
| organization | 3000 | 60s |
| credential | 10000 | 60s |
| destructive | 50 | 3600s |
每个计入限额的响应都带有 RateLimit 与 RateLimit-Policy 请求头,指明最接近上限的那个计数器。被拒绝时还会附带 Retry-After,请按它等待,而不要自行猜测。
未携带可用密钥的请求由另一个严格得多的计数器衡量:每个客户端地址每分钟 60 次请求。它对有效密钥从不适用。
请求体积
超出所属端点上限的请求体会以 payload_too_large 被拒绝,此时还没有任何内容被读向数据库。上限检查两次:先看声明的 Content-Length,这一步不花任何代价;再看真正到达的字节数,因此不声明长度的请求同样受限。
| 端点 | 上限 |
|---|---|
| 写入单条记录的所有端点 | 256 KiB (262144 bytes) |
| 批量导入与单元批次 | 4 MiB (4194304 bytes) |
批量上限取自本 API 接受的最大批次,而不是一个凑整的数字:一个类别中每个字段都填满的 500 行导入,或 1000 个序列化单元。两者都塞不进标准上限,这正是那两个端点各有自己上限的原因。
请求体必须以 application/json 到达,application/merge-patch+json 同样接受。其他类型会以 unsupported_media_type 被拒绝,而不是在解析器内部失败——值得知道的情形是 curl -d:除非另行指定,它发送的是 application/x-www-form-urlencoded。完全不带 Content-Type 的请求体按 JSON 读取。
HTTP 方法
端点只回应自己记录在案的方法。其他方法一律以 method_not_allowed 拒绝,并附带列出可用方法的 Allow 头,动词用错的客户端可以直接从这条拒绝里读到正确的那个。GET 能用的地方 HEAD 都能用,OPTIONS 处处可用。
HTTP/1.1 405 Method Not Allowed
Allow: GET, HEAD, PATCH, OPTIONS
Content-Type: application/json
{
"error": {
"code": "method_not_allowed",
"documentation_url": "https://passportcraft.com/docs/api/errors#method_not_allowed",
"request_id": "req_9Fv2KpQ0aXbT4Lmn",
"details": { "method": "DELETE", "allowed": ["GET", "HEAD", "PATCH", "OPTIONS"] }
}
}破坏性操作
取消发布护照和把护照移入回收站,都会把一条已发布的记录撤出市场,因此二者在普通速率限制之外还另计一个上限。一串成本低廉、格式规整的请求,不应当有能力让整份目录集体下线。
为字段附加文件
部分 url 字段除链接外也接受上传的文件,例如检测报告或符合性声明。请先上传文件,然后把文件 ID 写入 `data` 中该字段的配套键,即可让字段指向它。字段本身可以留空:可附文件的字段一旦附上文件即视为已填写,因此 url 为空的护照仍可通过校验并发布。
您无需自行拼接该键名。接受文件的字段在类别架构中带有两个属性:`document_types` 列出可接受的文件类型,`document_id_key` 给出应当写入的确切键名。请从架构中读取键名,而不要依赖命名规律推断——前者是唯一受支持的做法。配套键是字段的一个属性,而非独立字段,因此不会出现在架构的 `fields` 列表中。
有一条规则架构不会替您写明:文件的 `access_tier` 必须与字段一致。公开级文件无法满足 authority 级字段,附加会以 `access_tier_mismatch` 被拒绝——因为级别与字段不一致的文件,本来就不会显示在该字段出现的位置。请按字段描述中给出的 `access_tier` 创建文件。
curl "https://passportcraft.com/api/v1/organizations/{organization}/passports/{id}" \
-X PATCH \
-H "Authorization: Bearer $PASSPORTCRAFT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"data":{"conformity_declaration_url__document_id":"doc_123"}}'清空配套键即可解除附加;文件本身会保留在您的文件库中,直至您将其删除。若删除仍被某个字段指向的文件,响应会说明解除了哪些关联,而该说明只会返回给发起该请求的调用方。
文档的 `upload_state` 取值为 `ready`、`pending` 或 `unknown`。`unknown` 表示我们无法确定字节是否已存入存储:可能是存储未响应,也可能是该行超出了单次请求最多探测的 25 个无字节文档。请把它视为尚未确定,而不是文件缺失:发布时 `unknown` 会返回 `storage_unavailable`,而不会声称文件缺失;稍后再读取通常即可确定。
请求标识符
无论成功还是失败,每个响应都带有 Request-Id 请求头。在支持工单中附上它,就能精确定位到那一次调用。
事件流保留 30 天的历史。轮询间隔长于这个期限的集成会漏掉变更,应改为与护照列表做对账。