稳定性: 测试版
通过 REST 端到端渲染一张发票
通过 NextPDF Connect 的 Representational State Transfer(REST,表述性状态转移)接口,把一张发票从 JSON 一路推进到本地验证过的 PDF,一次一笔连接层交换。这份 recipe(示例)会把渲染工作提交到 POST /api/v1/jobs,以相同的 Idempotency-Key 重放提交,借此展示重复也安全的路径,轮询 GET /api/v1/jobs/{id},从 GET /api/v1/jobs/{id}/result 下载 PDF,用 qpdf --check 检查字节,最后删除已完成的工作。
下方的每一段响应都是从一套真实的 core 分级 Connect 部署(RoadRunner 下的 nextpdf/server,绑定到 http://localhost:8080)逐字捕获而来的。唯一的替换是 API 密钥,以 $NEXTPDF_CONNECT_TOKEN 环境变量的形式呈现;工作标识符、请求标识符、时间戳、标头和正文字节都完全是服务器返回的内容。诸如 Date 和 X-Request-Id 之类的标头值在你的部署上当然会有所不同。
这份 recipe 只驱动一份文档,方便你完整地阅读每一次交换。若需处理多份文档、有界并发以及由 Retry-After 驱动的轮询循环,请参阅批量生成 PDF 并跟踪进度,它使用同一套工作接口。
服务器端使用标准的 Connect 发行版:
composer require nextpdf/server这份 recipe 的客户端只需要 curl 加上 qpdf,因此你可以把它移植到任何 HTTP 客户端。先为你的部署导出各项取值:
export NEXTPDF_CONNECT_URL="http://localhost:8080"export NEXTPDF_CONNECT_TOKEN="npk_live_{kid}_{secret}" # your real key# Key provisioning and server startup live in the quickstart:概念概览
标题为“概念概览”的章节异步工作接口把提交与获取分离开:你提交一个渲染请求,收到一条工作记录,并在工作进入 completed 时取回结果。渲染请求本身是一个有序的 operations 数组——正是那些在每种传输上支撑 Connect 工具调用的操作类型(set_font、add_text、add_table、add_image、add_page)——再加上文档级字段(page_size、orientation、title、author)。
有两处契约细节塑造了你即将阅读的这段记录:
- 幂等提交。 以
Idempotency-Key作为键的提交在第一次返回201 Created,在重放时返回200 OK并带有同一条工作记录,因此网络重试绝不会渲染两次。 - 提交可能已经是终止状态。 当前版本会在回应
POST之前就地处理该工作,因此提交响应可能已经带有status: "completed"——正如下方所示。轮询直至终止的契约才是稳定的 API 形态:写好轮询循环,并在任何一次尝试(包括第一次)中接受终止状态。
在提交任何内容之前,你可以先确认你的部署对外暴露了什么:GET /api/v1/capabilities 会返回你的 API 密钥所属分级可以使用的操作目录。在这里捕获的 core 分级部署上,它只列出了 core 操作;真正权威的目录始终是运行中服务器自身的响应,而非本页。
API 接口
标题为“API 接口”的章节| 交换 | 方法与路径 | 捕获到的状态 |
|---|---|---|
| 提交渲染工作 | POST /api/v1/jobs | 201 Created |
| 重放相同的提交 | POST /api/v1/jobs(相同的 Idempotency-Key) | 200 OK |
| 轮询工作记录 | GET /api/v1/jobs/{id} | 200 OK |
| 下载 PDF | GET /api/v1/jobs/{id}/result | 200 OK,application/pdf |
| 删除已完成的工作 | DELETE /api/v1/jobs/{id} | 204 No Content |
每个 /api/v1/* 请求都通过一枚承载令牌(bearer token)进行身份验证:Authorization: Bearer npk_live_{kid}_{secret}。成功的 JSON 响应共享 { "data": ..., "meta": ... } 信封;你据以行动的字段位于 data 之下。
发票请求
标题为“发票请求”的章节把渲染请求写入 invoice.json。它是一份朴素、确定性的操作列表——一行加粗的抬头、一行开票信息,以及一张明细表格:
{ "page_size": "A4", "orientation": "portrait", "title": "Invoice INV-2026-0042", "author": "Aurora Fixtures Ltd.", "operations": [ { "type": "set_font", "family": "helvetica", "style": "B", "size": 16 }, { "type": "add_text", "text": "Invoice INV-2026-0042" }, { "type": "set_font", "family": "helvetica", "style": "", "size": 10 }, { "type": "add_text", "text": "Issued 2026-07-08 by Aurora Fixtures Ltd. Payment is due within 30 days.", "width": 0, "line_height": 5 }, { "type": "add_table", "html": "<table><tr><th>Item</th><th>Qty</th><th>Unit price</th><th>Amount</th></tr><tr><td>Cable tray, 300 mm</td><td>12</td><td>18.40</td><td>220.80</td></tr><tr><td>Mounting kit</td><td>4</td><td>9.75</td><td>39.00</td></tr><tr><td>Site delivery</td><td>1</td><td>25.00</td><td>25.00</td></tr><tr><td>Total (EUR)</td><td></td><td></td><td>284.80</td></tr></table>" } ]}这里的发票字段是样例数据。每个操作的权威参数形态,就是你的部署所报告的那一份——在 MCP 上,tools/list 会为这个请求所用的每种操作类型返回完整的输入 schema(模式)。
端到端记录
标题为“端到端记录”的章节1. 提交渲染工作
标题为“1. 提交渲染工作”的章节curl -sS -i -X POST "$NEXTPDF_CONNECT_URL/api/v1/jobs" \ -H "Authorization: Bearer $NEXTPDF_CONNECT_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: inv-2026-0042" \ --data-binary @invoice.json服务器回应 201 Created:
HTTP/1.1 201 CreatedCache-Control: no-storeContent-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'Content-Type: application/json; charset=utf-8Referrer-Policy: no-referrerVary: Accept-EncodingX-Content-Type-Options: nosniffX-Frame-Options: DENYX-Powered-By: NextPDF ConnectX-Ratelimit-Remaining: 99X-Request-Id: 019f3fd6-c6ed-727c-958e-2fa4370ba97eDate: Wed, 08 Jul 2026 03:47:48 GMTTransfer-Encoding: chunked{ "data": { "job_id": "job_9bd0808960f10eb568484acc", "status": "completed", "created_at": "2026-07-08T03:47:48+00:00", "started_at": "2026-07-08T03:47:48+00:00", "completed_at": "2026-07-08T03:47:48+00:00", "result_url": "/api/v1/jobs/job_9bd0808960f10eb568484acc/result" }, "meta": { "request_id": "019f3fd6-c6ed-727c-958e-2fa4370ba97e", "timestamp": "2026-07-08T03:47:48+00:00", "duration_ms": 63.31, "api_version": "v1" }}在这次捕获中,工作已经是终止状态——status 为 "completed" 且 result_url 已存在——因为当前版本会在回应之前就地渲染。不要依赖这一点:把提交响应当作第一次轮询结果,并像对待任何其他轮询那样根据 data.status 分支处理。
2. 重放提交(幂等路径)
标题为“2. 重放提交(幂等路径)”的章节重试完全相同的命令——相同的 Idempotency-Key、相同的正文:
curl -sS -i -X POST "$NEXTPDF_CONNECT_URL/api/v1/jobs" \ -H "Authorization: Bearer $NEXTPDF_CONNECT_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: inv-2026-0042" \ --data-binary @invoice.json服务器返回 200 OK——而非 201——带有相同的 job_id,且不会发生第二次渲染(把 meta.duration_ms 与第一次响应作对比):
HTTP/1.1 200 OKCache-Control: no-storeContent-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'Content-Type: application/json; charset=utf-8Referrer-Policy: no-referrerVary: Accept-EncodingX-Content-Type-Options: nosniffX-Frame-Options: DENYX-Powered-By: NextPDF ConnectX-Ratelimit-Remaining: 99X-Request-Id: 019f3fd6-c756-7226-bc85-b255d75bd449Date: Wed, 08 Jul 2026 03:47:48 GMTTransfer-Encoding: chunked{ "data": { "job_id": "job_9bd0808960f10eb568484acc", "status": "completed", "created_at": "2026-07-08T03:47:48+00:00", "started_at": "2026-07-08T03:47:48+00:00", "completed_at": "2026-07-08T03:47:48+00:00", "result_url": "/api/v1/jobs/job_9bd0808960f10eb568484acc/result" }, "meta": { "request_id": "019f3fd6-c756-7226-bc85-b255d75bd449", "timestamp": "2026-07-08T03:47:48+00:00", "duration_ms": 1.03, "api_version": "v1" }}3. 轮询工作记录
标题为“3. 轮询工作记录”的章节curl -sS -i "$NEXTPDF_CONNECT_URL/api/v1/jobs/job_9bd0808960f10eb568484acc" \ -H "Authorization: Bearer $NEXTPDF_CONNECT_TOKEN"HTTP/1.1 200 OKCache-Control: no-storeContent-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'Content-Type: application/json; charset=utf-8Referrer-Policy: no-referrerVary: Accept-EncodingX-Content-Type-Options: nosniffX-Frame-Options: DENYX-Powered-By: NextPDF ConnectX-Ratelimit-Remaining: 98X-Request-Id: 019f3fd7-0038-7131-b71e-4c69e637233bDate: Wed, 08 Jul 2026 03:48:02 GMTTransfer-Encoding: chunked{ "data": { "job_id": "job_9bd0808960f10eb568484acc", "status": "completed", "created_at": "2026-07-08T03:47:48+00:00", "started_at": "2026-07-08T03:47:48+00:00", "completed_at": "2026-07-08T03:47:48+00:00", "result_url": "/api/v1/jobs/job_9bd0808960f10eb568484acc/result" }, "meta": { "request_id": "019f3fd7-0038-7131-b71e-4c69e637233b", "timestamp": "2026-07-08T03:48:02+00:00", "duration_ms": 0.24, "api_version": "v1" }}这次轮询显示的是一条终止记录,因此既没有 Retry-After 标头,也没有 poll_url 字段。而当工作仍处于 pending 或 running 时,服务器会在每次轮询时都设置 Retry-After(一个 2 秒的间隔)——请遵循它,而不要在密集循环中轮询。
4. 下载 PDF
标题为“4. 下载 PDF”的章节curl -sS -D result-headers.txt \ "$NEXTPDF_CONNECT_URL/api/v1/jobs/job_9bd0808960f10eb568484acc/result" \ -H "Authorization: Bearer $NEXTPDF_CONNECT_TOKEN" \ -o invoice-inv-2026-0042.pdfHTTP/1.1 200 OKCache-Control: no-storeContent-Disposition: attachment; filename="job-job_9bd0808960f10eb568484acc.pdf"Content-Length: 3663Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'Content-Type: application/pdfReferrer-Policy: no-referrerVary: Accept-EncodingX-Content-Type-Options: nosniffX-Frame-Options: DENYX-Powered-By: NextPDF ConnectX-Ratelimit-Remaining: 98X-Request-Id: 019f3fd7-0056-711e-acad-9f7ac5f21ba7Date: Wed, 08 Jul 2026 03:48:02 GMT正文是 PDF 二进制内容——在这次捕获中为 3,663 字节,与 Content-Length 标头一致——在此从略。它被写入 invoice-inv-2026-0042.pdf。
5. 在本地验证下载的字节
标题为“5. 在本地验证下载的字节”的章节一个带 Content-Type: application/pdf 的 200,就其本身而言,并不能证明正文是一份格式良好的 PDF。用 qpdf 运行一次结构检查:
qpdf --check invoice-inv-2026-0042.pdf针对上面下载的文件所捕获到的输出:
checking invoice-inv-2026-0042.pdfPDF Version: 2.0File is not encryptedFile is not linearizedNo syntax or stream encoding errors found; the file may still containerrors that qpdf cannot detectqpdf 自己的措辞就是那条诚实的边界:这是一次语法与流的检查,而非针对任何标准的符合性判定。
6. 删除已完成的工作
标题为“6. 删除已完成的工作”的章节curl -sS -i -X DELETE "$NEXTPDF_CONNECT_URL/api/v1/jobs/job_9bd0808960f10eb568484acc" \ -H "Authorization: Bearer $NEXTPDF_CONNECT_TOKEN"HTTP/1.1 204 No ContentCache-Control: no-storeContent-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'Content-Type: application/json; charset=utf-8Referrer-Policy: no-referrerVary: Accept-EncodingX-Content-Type-Options: nosniffX-Frame-Options: DENYX-Powered-By: NextPDF ConnectX-Ratelimit-Remaining: 97X-Request-Id: 019f3fd7-0084-722f-bf54-3dabead8aeeaDate: Wed, 08 Jul 2026 03:48:02 GMT删除之后,工作记录及其存储的结果都不复存在;随后对该工作的 GET 会返回 404。
边界情况与陷阱
标题为“边界情况与陷阱”的章节- 根据
data.status分支,而不要只看 HTTP 状态。 提交、重放和轮询都会带着一条工作记录返回2xx;生命周期状态位于data.status(pending、running、completed、failed、cancelled)。 - 重放的键搭配不同的正文会得到
409 Conflict。 幂等的200重放只有在正文与原始提交一致时才会发生。切勿把一把键重用于不同的内容。 - 完成前访问
/result会得到409。 只在轮询显示completed之后再下载。这个409是需要审视的正常响应,而非传输故障——正是每份 Connect recipe 都遵循的那套传输层与状态层分离(参阅 recipe 惯例)。 - 工作按所有者作用域隔离。 用一把 API 密钥提交的工作,对另一把密钥不可见:跨所有者的
GET返回404而非403。用你提交时所用的凭据去轮询。 progress可能不存在。 捕获到的记录没有progress字段,因为该工作已经是终止状态。当服务器为某个非终止工作跟踪进度时,data.progress是一个从 0 到 100 的整数;请把缺失的字段视为未知,而非零。failed的工作会带有data.error。 记录它;不要盲目重新提交。
一个渲染工作的成本是一次提交、至多寥寥几次轮询和一次下载。捕获到的 meta.duration_ms 值说明了这一点:在提交时用 63.31 毫秒渲染发票,用 1.03 毫秒完成那次未做任何实际渲染的幂等重放,以及亚毫秒级的状态读取。请按服务器的 Retry-After 节奏轮询,而不要用密集循环;状态读取虽廉价但并非免费,而且速率限制器会把它计入预算(观察捕获到的标头中 X-Ratelimit-Remaining 的递减)。对于批量处理,请对在途工作数量设界,而不要一次性提交所有内容——批量 recipe 实现了那个循环。
安全说明
标题为“安全说明”的章节- 只把承载令牌放在
Authorization标头里。 绝不放进查询字符串、日志行或已提交的文件。上面的记录正是出于这一原因用一个环境变量作了替换。 - 在信任下载的字节之前先验证它们。 第 5 步是流程的一部分,而非可选附加项:在归档或转发之前,确认响应是一份 PDF(至少要有
%PDF标头,用qpdf --check做结构检查)。 - 删除你不再需要的已完成工作。 第 6 步会把存储的结果从服务器移除;否则,一个已完成的工作会一直可供下载,直到服务器的工作垃圾回收将其清除。
- 使用最小权限的密钥。 这套流程需要一把 core 分级的渲染密钥,仅此而已。
符合性
标题为“符合性”的章节这份 recipe 不提出任何规范性标准主张。它会使用 Connect 异步工作 REST 端点,并读取服务器所定义的工作记录字段。qpdf --check 那一步只确认结构完整性——“the file may still contain errors that qpdf cannot detect” 是 qpdf 自己的告诫,已在上面逐字引用。判定是否符合某项标准(PDF/A-4、PDF/UA)是独立验证器的职责,且属于不同的接口——那条边界请参阅运行具名标准检查。
另请参阅
标题为“另请参阅”的章节- 批量生成 PDF 并跟踪进度:把同一套工作接口作为有界并发的批量处理来驱动。
- 生成你的第一份 PDF:最小的 Connect 渲染。
- 通过 MCP 驱动一个代理式文档会话:同一个引擎,逐个工具,通过 MCP stdio 传输。
- Connect recipe 惯例:每份 Connect recipe 都遵循的传输、分级与符合性契约。
- 通过 Connect 的例外感知错误处理:如何把传输故障与非成功状态区分开。