跳到內容
getnextpdf.com

穩定性: 測試版

透過 REST 端對端轉譯一份發票

透過 NextPDF Connect 的 Representational State Transfer(REST)介面,一次一筆連線交換,將一份發票從 JSON 轉為經本機驗證的 PDF。本範例將一項轉譯工作提交至 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 環境變數表示;工作識別碼、請求識別碼、時間戳記、標頭與主體位元組全都正是伺服器所回傳的內容。諸如 DateX-Request-Id 之類的標頭值,在您的部署上當然會有所不同。

本範例僅驅動單一文件,讓您能完整閱讀每一次交換。若要處理多份文件、有界並行, 以及由 Retry-After 驅動的輪詢迴圈,請參閱 批次產生 PDF 並追蹤進度, 其使用相同的工作介面。

伺服器端是標準的 Connect 發行版本:

Terminal window
composer require nextpdf/server

本範例的用戶端是 curl 加上 qpdf,因此您可以將其移植到任何 HTTP 用戶端。 請先匯出您部署環境的值:

/docs/connect/quickstart/
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_fontadd_textadd_tableadd_imageadd_page)——再加上文件層級的欄位(page_sizeorientationtitleauthor)。

有兩項合約細節形塑了您即將閱讀的逐字紀錄:

  • 等冪提交。Idempotency-Key 標記的提交第一次會回傳 201 Created,而在重播時則回傳 200 OK 並帶有相同的工作記錄,因此網路重試絕不會轉譯兩次。
  • 提交可能已處於終端狀態。 目前的版本會在回應 POST 之前就地處理該工作, 因此提交回應可能已帶有 status: "completed"——如下方所示。輪詢直到終端狀態的合約才是穩定的 API 形態:撰寫輪詢迴圈,並在任何一次嘗試(包括第一次)接受終端狀態。

在提交任何內容之前,您可以確認您的部署所公開的內容:GET /api/v1/capabilities 會回傳您 API 金鑰的層級所能觸及的操作目錄。在此處擷取的 core 層級部署上,它僅列出 core 操作;準據目錄始終是執行中伺服器本身的回應,而非本頁面。

交換方法與路徑擷取的狀態
提交轉譯工作POST /api/v1/jobs201 Created
重播相同的提交POST /api/v1/jobs(相同的 Idempotency-Key200 OK
輪詢工作記錄GET /api/v1/jobs/{id}200 OK
下載 PDFGET /api/v1/jobs/{id}/result200 OKapplication/pdf
刪除已完成的工作DELETE /api/v1/jobs/{id}204 No Content

每一筆 /api/v1/* 請求都以 bearer 權杖進行驗證: 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 會為此請求所使用的每一種操作類型回傳完整的輸入結構描述。

Terminal window
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 Created
Cache-Control: no-store
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'
Content-Type: application/json; charset=utf-8
Referrer-Policy: no-referrer
Vary: Accept-Encoding
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-Powered-By: NextPDF Connect
X-Ratelimit-Remaining: 99
X-Request-Id: 019f3fd6-c6ed-727c-958e-2fa4370ba97e
Date: Wed, 08 Jul 2026 03:47:48 GMT
Transfer-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 進行分支。

重試完全相同的命令——相同的 Idempotency-Key、相同的主體:

Terminal window
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 OK
Cache-Control: no-store
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'
Content-Type: application/json; charset=utf-8
Referrer-Policy: no-referrer
Vary: Accept-Encoding
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-Powered-By: NextPDF Connect
X-Ratelimit-Remaining: 99
X-Request-Id: 019f3fd6-c756-7226-bc85-b255d75bd449
Date: Wed, 08 Jul 2026 03:47:48 GMT
Transfer-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"
}
}
Terminal window
curl -sS -i "$NEXTPDF_CONNECT_URL/api/v1/jobs/job_9bd0808960f10eb568484acc" \
-H "Authorization: Bearer $NEXTPDF_CONNECT_TOKEN"
HTTP/1.1 200 OK
Cache-Control: no-store
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'
Content-Type: application/json; charset=utf-8
Referrer-Policy: no-referrer
Vary: Accept-Encoding
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-Powered-By: NextPDF Connect
X-Ratelimit-Remaining: 98
X-Request-Id: 019f3fd7-0038-7131-b71e-4c69e637233b
Date: Wed, 08 Jul 2026 03:48:02 GMT
Transfer-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 欄位。 當工作仍為 pendingrunning 時,伺服器會在每一次輪詢時設定 Retry-After (2 秒的間隔)——請遵循它,而非以緊密迴圈輪詢。

Terminal window
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.pdf
HTTP/1.1 200 OK
Cache-Control: no-store
Content-Disposition: attachment; filename="job-job_9bd0808960f10eb568484acc.pdf"
Content-Length: 3663
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'
Content-Type: application/pdf
Referrer-Policy: no-referrer
Vary: Accept-Encoding
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-Powered-By: NextPDF Connect
X-Ratelimit-Remaining: 98
X-Request-Id: 019f3fd7-0056-711e-acad-9f7ac5f21ba7
Date: Wed, 08 Jul 2026 03:48:02 GMT

主體是 PDF 二進位內容——在此擷取中為 3,663 個位元組,與 Content-Length 標頭相符——並在此處省略。它會被寫入 invoice-inv-2026-0042.pdf

一個帶有 Content-Type: application/pdf200,本身並不能證明主體是格式正確的 PDF。請以 qpdf 執行結構檢查:

Terminal window
qpdf --check invoice-inv-2026-0042.pdf

上方所下載檔案的擷取輸出:

checking invoice-inv-2026-0042.pdf
PDF Version: 2.0
File is not encrypted
File is not linearized
No syntax or stream encoding errors found; the file may still contain
errors that qpdf cannot detect

qpdf 自身的用詞是誠實的界線:這是語法與串流檢查,而非針對任何標準的符合性判定。

Terminal window
curl -sS -i -X DELETE "$NEXTPDF_CONNECT_URL/api/v1/jobs/job_9bd0808960f10eb568484acc" \
-H "Authorization: Bearer $NEXTPDF_CONNECT_TOKEN"
HTTP/1.1 204 No Content
Cache-Control: no-store
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'
Content-Type: application/json; charset=utf-8
Referrer-Policy: no-referrer
Vary: Accept-Encoding
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-Powered-By: NextPDF Connect
X-Ratelimit-Remaining: 97
X-Request-Id: 019f3fd7-0084-722f-bf54-3dabead8aeea
Date: Wed, 08 Jul 2026 03:48:02 GMT

刪除之後,工作記錄及其儲存的結果都會消失;後續對該工作進行的 GET 會回傳 404

  • data.status 分支,而非僅依 HTTP 狀態。 提交、重播與輪詢全都會回傳帶有工作記錄的 2xx;生命週期狀態位於 data.statuspendingrunningcompletedfailedcancelled)。
  • 以不同主體重播同一組金鑰,會得到 409 Conflict 只有在主體與原始提交相符時, 才會發生等冪的 200 重播。切勿為不同的內容重用金鑰。
  • 在完成之前存取 /result 會是 409 只有在輪詢顯示 completed 之後才下載。409 是需要檢視的一般回應,並非傳輸失敗——這正是每個 Connect 範例都遵循的「傳輸與狀態」的區分(請參閱 範例慣例)。
  • 工作以擁有者為範圍。 在某一 API 金鑰下提交的工作,對另一組金鑰而言是不可見的:跨擁有者的 GET 會回傳 404,而非 403。請以您提交時所用的憑證進行輪詢。
  • progress 可能不存在。 擷取的記錄並未帶有 progress 欄位,因為該工作已處於終端狀態。當伺服器為非終端工作追蹤進度時,data.progress 是一個從 0 到 100 的整數;請將缺少的欄位視為未知,而非零。
  • failed 工作會帶有 data.error 請將其記錄下來;切勿盲目重新提交。

一項轉譯工作的成本是一次提交、至多寥寥數次輪詢,以及一次下載。擷取的 meta.duration_ms 值道盡了一切:提交時轉譯發票花費 63.31 ms、未做任何工作的等冪重播花費 1.03 ms,而狀態讀取則在毫秒以下。請依伺服器的 Retry-After 節奏輪詢,而非以緊密迴圈進行;狀態讀取雖便宜卻非免費,且速率限制器會將其計入預算 (請留意擷取標頭中 X-Ratelimit-Remaining 逐次遞減)。對於批次,請對進行中的工作設限,而非一次提交全部——批次範例 便實作了該迴圈。

  • 僅將 bearer 權杖保留在 Authorization 標頭中。 切勿放在查詢字串、日誌行或已提交的檔案裡。上方的逐字紀錄正是基於這個原因而以環境變數替換。
  • 在信任下載的位元組之前先驗證它們。 步驟 5 是流程的一部分,並非可有可無的附加項:在封存或轉送之前,請確認回應是 PDF(最少要有 %PDF 標頭,並以 qpdf --check 檢查結構)。
  • 刪除您不再需要的已完成工作。 步驟 6 會從伺服器移除已儲存的結果;否則,已完成的工作在伺服器的工作垃圾回收將其移除之前,仍可持續下載。
  • 使用最小權限金鑰。 此流程只需要一組 core 層級的轉譯金鑰,別無其他。

本範例未做出任何規範性標準主張。它演練 Connect 非同步工作的 REST 端點,並讀取伺服器所定義的工作記錄欄位。qpdf --check 步驟僅確認結構完整性——「the file may still contain errors that qpdf cannot detect」是 qpdf 自身的但書,已於上方逐字引用。判定是否符合某項標準(PDF/A-4、PDF/UA)是獨立驗證器的工作,且屬於不同的介面——關於該界線,請參閱 執行具名標準檢查