Lewati ke konten
getnextpdf.com

stabilitas: Beta

Render faktur secara menyeluruh melalui REST

Ubah satu faktur dari JSON menjadi PDF yang terverifikasi secara lokal melalui permukaan Representational State Transfer (REST) NextPDF Connect, satu pertukaran wire pada satu waktu. Resep ini mengirim job render ke POST /api/v1/jobs, mengulang pengiriman dengan Idempotency-Key yang sama untuk menunjukkan jalur yang aman terhadap duplikat, melakukan polling GET /api/v1/jobs/{id}, mengunduh PDF dari GET /api/v1/jobs/{id}/result, memeriksa byte dengan qpdf --check, dan menghapus job yang telah selesai.

Setiap respons di bawah adalah tangkapan verbatim dari deployment Connect core-tier yang nyata (nextpdf/server di bawah RoadRunner, terikat ke http://localhost:8080). Satu-satunya penggantian adalah kunci API, yang ditampilkan sebagai variabel lingkungan $NEXTPDF_CONNECT_TOKEN; identifier job, identifier permintaan, stempel waktu, header, dan byte body persis seperti yang dikembalikan server. Nilai header seperti Date dan X-Request-Id tentu akan berbeda pada deployment Anda.

Resep ini memproses satu dokumen agar Anda dapat membaca setiap pertukaran secara lengkap. Untuk banyak dokumen, konkurensi terbatas, dan loop polling yang digerakkan oleh Retry-After, lihat Buat PDF secara batch dengan pelacakan progres, yang menggunakan permukaan job yang sama.

Sisi server adalah distribusi Connect standar:

Terminal window
composer require nextpdf/server

Sisi klien dari resep ini adalah curl ditambah qpdf, sehingga Anda dapat memindahkannya ke klien HTTP mana pun. Ekspor nilai deployment Anda terlebih dahulu:

/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:

Permukaan async-job memisahkan pengiriman dari pengambilan: Anda mengirim permintaan render, menerima rekaman job, dan mengambil hasilnya ketika job mencapai completed. Permintaan render itu sendiri adalah array operations yang berurutan — tipe operasi yang sama (set_font, add_text, add_table, add_image, add_page) yang mendukung pemanggilan tool Connect pada setiap transport — ditambah field tingkat-dokumen (page_size, orientation, title, author).

Dua detail kontrak membentuk transkrip yang akan Anda baca:

  • Pengiriman idempoten. Pengiriman yang diberi kunci Idempotency-Key mengembalikan 201 Created pada kali pertama dan 200 OK dengan rekaman job yang sama ketika diulang, sehingga percobaan ulang jaringan tidak pernah merender dua kali.
  • Pengiriman mungkin sudah terminal. Rilis saat ini memproses job secara inline sebelum menjawab POST, sehingga respons pengiriman bisa saja sudah membawa status: "completed" — seperti yang terjadi di bawah. Kontrak poll-hingga-terminal adalah bentuk API yang stabil: tulis loop polling, dan terima status terminal pada percobaan mana pun, termasuk yang pertama.

Anda dapat memastikan apa yang diekspos deployment Anda sebelum mengirim apa pun: GET /api/v1/capabilities mengembalikan katalog operasi yang dapat dijangkau oleh tier kunci API Anda. Pada deployment core-tier yang ditangkap di sini, katalog itu hanya mencantumkan operasi core; katalog resmi selalu merupakan respons server yang sedang berjalan itu sendiri, bukan halaman ini.

PertukaranMetode dan pathStatus yang ditangkap
Kirim job renderPOST /api/v1/jobs201 Created
Ulangi pengiriman yang samaPOST /api/v1/jobs (Idempotency-Key yang sama)200 OK
Polling rekaman jobGET /api/v1/jobs/{id}200 OK
Unduh PDFGET /api/v1/jobs/{id}/result200 OK, application/pdf
Hapus job yang telah selesaiDELETE /api/v1/jobs/{id}204 No Content

Autentikasi berupa bearer token pada setiap permintaan /api/v1/*: Authorization: Bearer npk_live_{kid}_{secret}. Respons JSON yang berhasil menggunakan envelope { "data": ..., "meta": ... } yang sama; field yang Anda gunakan berada di bawah data.

Tulis permintaan render ke invoice.json. Ini adalah daftar operasi yang sederhana dan deterministik — sebuah baris header tebal, baris penerbitan, dan tabel item baris:

{
"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>" }
]
}

Field faktur di sini adalah data contoh. Bentuk argumen otoritatif untuk setiap operasi adalah yang dilaporkan oleh deployment Anda — melalui MCP, tools/list mengembalikan skema input lengkap untuk setiap tipe operasi yang digunakan permintaan ini.

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

Server menjawab 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"
}
}

Job sudah terminal dalam tangkapan ini — status bernilai "completed" dan result_url ada — karena rilis saat ini merender secara inline sebelum menjawab. Jangan bergantung pada hal itu: perlakukan respons pengiriman sebagai hasil polling pertama dan bercabang berdasarkan data.status seperti polling lainnya.

Ulangi persis perintah yang sama — Idempotency-Key yang sama, body yang sama:

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

Server mengembalikan 200 OK — bukan 201 — dengan job_id yang sama, dan tidak ada render kedua yang terjadi (bandingkan meta.duration_ms dengan respons pertama):

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"
}
}

Polling ini menunjukkan rekaman terminal, sehingga tidak ada header Retry-After dan tidak ada field poll_url. Selama job masih pending atau running, server menetapkan Retry-After (interval 2 detik) pada setiap polling — patuhi itu alih-alih melakukan polling dalam loop yang ketat.

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

Body-nya adalah biner PDF — 3.663 byte dalam tangkapan ini, cocok dengan header Content-Length — dan disingkat di sini. Berkas itu ditulis ke invoice-inv-2026-0042.pdf.

5. Verifikasi byte yang diunduh secara lokal

Bagian berjudul “5. Verifikasi byte yang diunduh secara lokal”

Sebuah 200 dengan Content-Type: application/pdf bukanlah, dengan sendirinya, bukti bahwa body adalah PDF yang well-formed. Jalankan pemeriksaan struktural dengan qpdf:

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

Keluaran yang ditangkap untuk berkas yang diunduh di atas:

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

Kata-kata qpdf sendiri adalah batas yang jujur: ini adalah pemeriksaan sintaks dan stream, bukan penentuan konformansi terhadap standar apa pun.

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

Setelah penghapusan, rekaman job dan hasil tersimpannya hilang; GET berikutnya pada job mengembalikan 404.

  • Bercabang berdasarkan data.status, bukan hanya berdasarkan status HTTP. Pengiriman, pengulangan, dan polling semuanya mengembalikan 2xx dengan rekaman job; status siklus hidup berada di data.status (pending, running, completed, failed, cancelled).
  • Kunci yang diulang dengan body berbeda menghasilkan 409 Conflict. Pengulangan idempoten 200 hanya terjadi ketika body cocok dengan pengiriman asli. Jangan pernah menggunakan kembali kunci untuk konten yang berbeda.
  • /result sebelum selesai menghasilkan 409. Unduh hanya setelah polling menunjukkan completed. 409 adalah respons normal untuk diperiksa, bukan kegagalan transport — pemisahan transport-versus-status yang sama yang diikuti setiap resep Connect (lihat Konvensi resep).
  • Job bersifat owner-scoped. Job yang dikirim dengan satu kunci API tidak terlihat oleh kunci lain: GET lintas-pemilik mengembalikan 404, bukan 403. Lakukan polling dengan kredensial yang Anda gunakan untuk mengirim.
  • progress mungkin tidak ada. Rekaman yang ditangkap tidak membawa field progress karena job sudah terminal. Ketika server melacak progres untuk job yang belum terminal, data.progress adalah integer dari 0 hingga 100; perlakukan field yang hilang sebagai tidak diketahui, bukan nol.
  • Job yang failed membawa data.error. Catat itu; jangan mengirim ulang secara membabi buta.

Satu job render membutuhkan satu pengiriman, paling banyak beberapa polling, dan satu unduhan. Nilai meta.duration_ms yang ditangkap menceritakan keseluruhan kisahnya: 63,31 ms untuk merender faktur saat pengiriman, 1,03 ms untuk pengulangan idempoten yang tidak melakukan pekerjaan apa pun, dan pembacaan status di bawah satu milidetik. Lakukan polling sesuai irama Retry-After server alih-alih loop yang ketat; pembacaan status memang murah tetapi tidak gratis, dan pembatas laju menganggarkannya (perhatikan X-Ratelimit-Remaining menurun dalam header yang ditangkap). Untuk batch, batasi job yang sedang berjalan alih-alih mengirim semuanya sekaligus — resep batch mengimplementasikan loop tersebut.

  • Simpan bearer token hanya di header Authorization. Jangan pernah di query string, baris log, atau berkas yang di-commit. Transkrip di atas menggantinya dengan variabel lingkungan justru karena alasan itu.
  • Validasi byte yang diunduh sebelum mempercayainya. Langkah 5 adalah bagian dari alur, bukan tambahan opsional: periksa bahwa respons adalah PDF (minimal header %PDF, qpdf --check untuk struktur) sebelum mengarsipkan atau meneruskannya.
  • Hapus job selesai yang tidak lagi Anda perlukan. Langkah 6 menghapus hasil tersimpan dari server; jika tidak, job yang telah selesai tetap dapat diunduh hingga garbage collection job pada server menghapusnya.
  • Gunakan kunci dengan hak istimewa paling kecil. Alur ini hanya membutuhkan kunci render core-tier, tidak lebih.

Resep ini tidak membuat klaim standar normatif. Resep ini menjalankan endpoint REST async-job Connect dan membaca field rekaman-job yang didefinisikan server. Langkah qpdf --check hanya mengonfirmasi integritas struktural — “the file may still contain errors that qpdf cannot detect” adalah peringatan qpdf sendiri, dikutip verbatim di atas. Menentukan konformansi terhadap suatu standar (PDF/A-4, PDF/UA) adalah tugas validator independen, dan permukaan yang berbeda — lihat Jalankan pemeriksaan standar bernama untuk batas tersebut.