コンテンツにスキップ
getnextpdf.com

安定性: ベータ

REST でインボイスをエンドツーエンドでレンダリングする

1 件のインボイスを、JSON からローカルで検証済みの PDF まで、NextPDF Connect の Representational State Transfer(REST)サーフェス上で、ワイヤー上のやり取りを 1 回ずつたどりながら処理します。このレシピでは、レンダリングジョブを POST /api/v1/jobs に送信し、同じ Idempotency-Key で送信を再実行して重複しても安全なパスを示し、GET /api/v1/jobs/{id} をポーリングし、 GET /api/v1/jobs/{id}/result から PDF をダウンロードし、qpdf --check でバイト列を確認して、完了したジョブを削除します。

以下のレスポンスはすべて、実際の Core ティアの Connect デプロイ (RoadRunner 上の nextpdf/serverhttp://localhost:8080 にバインド) から逐語的にキャプチャしたものです。唯一の置き換えは API キーで、 $NEXTPDF_CONNECT_TOKEN 環境変数として示しています。ジョブ識別子、 リクエスト識別子、タイムスタンプ、ヘッダー、ボディのバイト列は、サーバーが返したものそのままです。DateX-Request-Id などのヘッダー値は、 当然ながらお使いのデプロイでは異なります。

このレシピは 1 件の文書を扱うので、各やり取りを最後まで通して読むことができます。多数の文書、上限を設けた並行処理、Retry-After に基づくポーリングループについては、同じジョブサーフェスを使う 進捗追跡付きで PDF をバッチ生成する を参照してください。

サーバー側は、標準の Connect ディストリビューションです。

Terminal window
composer require nextpdf/server

このレシピのクライアント側は curlqpdf です。そのため、任意の 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)を加えたものです。

これから読むトランスクリプトを形作る、2 つの契約上の詳細があります。

  • 冪等な送信。 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
PDF をダウンロードするGET /api/v1/jobs/{id}/result200 OKapplication/pdf
完了したジョブを削除するDELETE /api/v1/jobs/{id}204 No Content

認証は、すべての /api/v1/* リクエストでベアラートークンを使います。 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

サーバーは、201 ではなく 200 OK を、同じ job_id とともに返します。 2 回目のレンダリングは発生しません(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 フィールドもありません。ジョブがまだ pending または running の間は、サーバーがすべてのポーリングに 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/pdf を伴う 200 は、それ自体では、ボディが整形式の 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

削除後は、ジョブレコードと保存された結果はなくなります。そのジョブに対する以降の GET404 を返します。

  • HTTP ステータスだけでなく、data.status で分岐してください。 送信、 再実行、ポーリングはいずれも、ジョブレコードとともに 2xx を返します。 ライフサイクルの状態は data.statuspendingrunningcompletedfailedcancelled)にあります。
  • 同じキーでボディを変えて再実行すると 409 Conflict になります。 冪等な 200 の再実行は、ボディが元の送信と一致する場合にのみ発生します。 異なる内容に対してキーを再利用しないでください。
  • 完了前の /result409 になります。 ポーリングが completed を示してからのみダウンロードしてください。この 409 は、トランスポートの失敗ではなく、確認すべき通常のレスポンスです。これは、すべての Connect レシピが従う、トランスポートとステータスの分離と同じです (レシピの規約を参照)。
  • ジョブはオーナースコープです。 ある API キーで送信したジョブは、 別のキーからは見えません。オーナーをまたぐ GET は、403 ではなく 404 を返します。送信に使用した資格情報でポーリングしてください。
  • progress が存在しないことがあります。 キャプチャしたレコードには progress フィールドがありません。ジョブが既に終端状態だったためです。 サーバーが非終端状態のジョブの進捗を追跡する場合、data.progress は 0 から 100 までの整数になります。フィールドが欠けている場合は、0 ではなく不明として扱ってください。
  • failed のジョブには data.error が入ります。 それを記録してください。 やみくもに再送信しないでください。

1 件のレンダリングジョブのコストは、1 回の送信、多くても数回のポーリング、 そして 1 回のダウンロードです。キャプチャした meta.duration_ms の値がその実態を示しています。送信時のインボイスのレンダリングに 63.31 ms、 何も処理しなかった冪等な再実行に 1.03 ms、ステータスの読み取りはサブミリ秒台です。タイトなループではなく、サーバーの Retry-After の間隔でポーリングしてください。ステータスの読み取りは安価ですが無料ではなく、 その分だけレートリミッターの予算を消費します(キャプチャしたヘッダーで X-Ratelimit-Remaining が減っていくのを確認してください)。バッチの場合は、すべてを一度に送信するのではなく、処理中のジョブ数に上限を設けてください。バッチレシピ がそのループを実装しています。

  • ベアラートークンは 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)への適合性の判定は、独立したバリデーターの仕事であり、別のサーフェスです。その境界については 名前付き標準チェックを実行する を参照してください。