ข้ามไปยังเนื้อหา
getnextpdf.com

ความเสถียร: เบต้า

เรนเดอร์ใบแจ้งหนี้จากต้นจนจบผ่าน REST

นำใบแจ้งหนี้หนึ่งรายการจาก JSON ไปสู่ PDF ที่ตรวจสอบแล้วในเครื่องผ่านพื้นผิว Representational State Transfer (REST) ของ NextPDF Connect ทีละการแลกเปลี่ยนบนสาย สูตรนี้ส่งงานเรนเดอร์ไปยัง POST /api/v1/jobs รีเพลย์การส่งด้วย Idempotency-Key เดียวกันเพื่อแสดงเส้นทางที่ปลอดภัยต่อการซ้ำ สำรวจสถานะที่ GET /api/v1/jobs/{id} ดาวน์โหลด PDF จาก GET /api/v1/jobs/{id}/result ตรวจสอบไบต์ด้วย qpdf --check และลบงานที่เสร็จแล้ว

การตอบกลับทุกรายการด้านล่างเป็นการจับแบบคำต่อคำจากดีพลอยเมนต์ Connect ระดับ core จริง (nextpdf/server ภายใต้ RoadRunner ผูกกับ http://localhost:8080) สิ่งเดียวที่ถูกแทนที่คือ API key ซึ่งแสดงเป็นตัวแปร สภาพแวดล้อม $NEXTPDF_CONNECT_TOKEN ส่วนตัวระบุงาน ตัวระบุคำขอ ไทม์สแตมป์ เฮดเดอร์ และไบต์ของเนื้อหาเป็นสิ่งที่เซิร์ฟเวอร์ส่งคืนมาจริงทุกประการ ค่าเฮดเดอร์เช่น Date และ X-Request-Id ย่อมแตกต่างกันไปในดีพลอยเมนต์ของคุณ

สูตรนี้ขับเคลื่อนเอกสารเพียงฉบับเดียวเพื่อให้อ่านการแลกเปลี่ยนแต่ละครั้งได้ครบถ้วน สำหรับเอกสารจำนวนมาก การทำงานพร้อมกันแบบมีขอบเขต และลูปการสำรวจสถานะที่ ขับเคลื่อนด้วย Retry-After ดู สร้าง PDF แบบกลุ่มพร้อมติดตามความคืบหน้า ซึ่งใช้พื้นผิวงานเดียวกัน

ฝั่งเซิร์ฟเวอร์คือชุดแจกจ่าย Connect มาตรฐาน:

Terminal window
composer require nextpdf/server

ฝั่งไคลเอนต์ของสูตรนี้คือ curl บวกกับ qpdf จึงนำไปปรับใช้กับไคลเอนต์ HTTP ใดก็ได้ ให้ export ค่าต่างๆ ของดีพลอยเมนต์ก่อน:

/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 ที่เรียงลำดับ ซึ่งเป็นชนิดของโอเปอเรชันเดียวกัน (set_font, add_text, add_table, add_image, add_page) ที่รองรับการเรียกเครื่องมือของ Connect บนทุกทรานสปอร์ต บวกกับฟิลด์ระดับเอกสาร (page_size, orientation, title, author)

รายละเอียดของสัญญาสองข้อกำหนดรูปแบบทรานสคริปต์ที่คุณกำลังจะได้อ่าน:

  • การส่งแบบ idempotent การส่งที่กำหนดคีย์ด้วย Idempotency-Key จะคืน 201 Created ในครั้งแรก และคืน 200 OK พร้อมเรคคอร์ดของงาน เดียวกันเมื่อรีเพลย์ การลองใหม่ทางเครือข่ายจึงไม่มีทางเรนเดอร์ซ้ำสองครั้ง
  • การส่งอาจอยู่ในสถานะสิ้นสุดแล้ว รุ่นปัจจุบันประมวลผลงานแบบอินไลน์ ก่อนตอบ POST การตอบกลับของการส่งจึงอาจมี status: "completed" อยู่แล้ว ดังที่ปรากฏด้านล่าง สัญญาแบบสำรวจจนกว่าจะสิ้นสุดคือรูปแบบ API ที่เสถียร: เขียนลูปสำรวจสถานะ และยอมรับสถานะสิ้นสุดในความพยายามใดก็ได้ รวมถึงครั้งแรก

คุณสามารถยืนยันสิ่งที่ดีพลอยเมนต์ของคุณเปิดเผยได้ก่อนส่งสิ่งใด: GET /api/v1/capabilities จะคืนแคตตาล็อกของโอเปอเรชันที่ระดับของ API key ของคุณเข้าถึงได้ บนดีพลอยเมนต์ระดับ core ที่จับไว้ที่นี่ แคตตาล็อกแสดงเฉพาะโอเปอเรชันของ core เท่านั้น แคตตาล็อกที่เป็นบันทึกอย่างเป็นทางการคือการตอบกลับของเซิร์ฟเวอร์ที่กำลัง ทำงานอยู่เสมอ ไม่ใช่หน้านี้

การแลกเปลี่ยนเมธอดและพาธสถานะที่จับได้
ส่งงานเรนเดอร์POST /api/v1/jobs201 Created
รีเพลย์การส่งเดิมPOST /api/v1/jobs (Idempotency-Key เดียวกัน)200 OK
สำรวจสถานะเรคคอร์ดของงานGET /api/v1/jobs/{id}200 OK
ดาวน์โหลด PDFGET /api/v1/jobs/{id}/result200 OK, application/pdf
ลบงานที่เสร็จแล้วDELETE /api/v1/jobs/{id}204 No Content

การยืนยันตัวตนคือ bearer token ในทุกคำขอ /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

เซิร์ฟเวอร์คืน 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 ขณะที่งานยังอยู่ในสถานะ 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

200 ที่มี Content-Type: application/pdf เพียงลำพังไม่ใช่ข้อพิสูจน์ว่า เนื้อหาเป็น 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.status (pending, running, completed, failed, cancelled)
  • คีย์ที่รีเพลย์ด้วยเนื้อหาที่ต่างกันจะได้ 409 Conflict การรีเพลย์แบบ idempotent ที่ได้ 200 จะเกิดขึ้นเฉพาะเมื่อเนื้อหาตรงกับ การส่งเดิมเท่านั้น อย่านำคีย์กลับมาใช้ซ้ำกับเนื้อหาที่ต่างกัน
  • การเรียก /result ก่อนงานเสร็จจะได้ 409 ดาวน์โหลดหลังจากการสำรวจ แสดง completed แล้วเท่านั้น 409 เป็นการตอบกลับปกติที่ควรตรวจสอบ ไม่ใช่ความล้มเหลวของทรานสปอร์ต เป็นการแยกทรานสปอร์ตออกจากสถานะแบบเดียวกับ ที่ทุกสูตรของ Connect ปฏิบัติตาม (ดู แนวปฏิบัติของสูตร)
  • งานมีขอบเขตตามเจ้าของ งานที่ส่งภายใต้ API key หนึ่งจะมองไม่เห็นจาก คีย์อื่น: การเรียก GET ข้ามเจ้าของจะคืน 404 ไม่ใช่ 403 ให้สำรวจด้วยข้อมูลรับรองที่ใช้ส่งงานนั้น
  • progress อาจไม่มีอยู่ เรคคอร์ดที่จับได้ไม่มีฟิลด์ progress เพราะงานอยู่ในสถานะสิ้นสุดแล้ว เมื่อเซิร์ฟเวอร์ติดตามความคืบหน้าของงานที่ ยังไม่สิ้นสุด data.progress จะเป็นจำนวนเต็มตั้งแต่ 0 ถึง 100 ให้ถือว่าฟิลด์ที่หายไปคือค่าไม่ทราบ ไม่ใช่ศูนย์
  • งานที่ failed จะมี data.error บันทึกไว้ อย่าส่งซ้ำโดยไม่พิจารณา

งานเรนเดอร์หนึ่งงานใช้การส่งหนึ่งครั้ง การสำรวจอย่างมากไม่กี่ครั้ง และการดาวน์โหลดหนึ่งครั้ง ค่า meta.duration_ms ที่จับได้บอกเล่าเรื่องราวนี้: 63.31 ms ในการเรนเดอร์ใบแจ้งหนี้ตอนส่ง 1.03 ms สำหรับการรีเพลย์แบบ idempotent ที่ไม่ได้ทำงานใด และการอ่านสถานะที่ใช้เวลาต่ำกว่าหนึ่งมิลลิวินาที ให้สำรวจตามจังหวะ Retry-After ของเซิร์ฟเวอร์แทนการวนซ้ำถี่ๆ การอ่านสถานะราคาถูกแต่ไม่ได้ฟรี และตัวจำกัดอัตราคิดงบให้กับมัน (สังเกต X-Ratelimit-Remaining ลดลงในเฮดเดอร์ที่จับได้) สำหรับงานแบบกลุ่ม ให้จำกัดจำนวนงานที่กำลังดำเนินการแทนการส่งทั้งหมดพร้อมกัน สูตรแบบกลุ่ม ได้ปรับใช้ลูปนั้น

  • เก็บ bearer token ไว้ในเฮดเดอร์ Authorization เท่านั้น อย่าใส่ในคิวรีสตริง บรรทัดล็อก หรือไฟล์ที่คอมมิต ทรานสคริปต์ข้างต้นแทนที่ด้วยตัวแปรสภาพแวดล้อมด้วยเหตุผลนี้เอง
  • ตรวจสอบไบต์ที่ดาวน์โหลดก่อนเชื่อถือ ขั้นตอนที่ 5 เป็นส่วนหนึ่งของ กระบวนการ ไม่ใช่ส่วนเสริมที่เลือกได้: ตรวจสอบว่าการตอบกลับเป็น PDF (อย่างน้อยต้องมีเฮดเดอร์ %PDF และ qpdf --check สำหรับโครงสร้าง) ก่อนเก็บถาวรหรือส่งต่อ
  • ลบงานที่เสร็จแล้วซึ่งไม่ต้องการอีกต่อไป ขั้นตอนที่ 6 ลบผลลัพธ์ที่จัดเก็บ ออกจากเซิร์ฟเวอร์ มิฉะนั้นงานที่เสร็จสมบูรณ์จะยังดาวน์โหลดได้จนกว่า การเก็บขยะของงานในเซิร์ฟเวอร์จะลบออก
  • ใช้คีย์ที่มีสิทธิ์น้อยที่สุด กระบวนการนี้ต้องการคีย์เรนเดอร์ระดับ core และไม่มีอะไรมากกว่านั้น

สูตรนี้ไม่ได้อ้างมาตรฐานเชิงบรรทัดฐานใด แต่เป็นการใช้งานเอนด์พอยต์ REST แบบงานอะซิงก์ของ Connect และอ่านฟิลด์ของเรคคอร์ดงานที่เซิร์ฟเวอร์กำหนดไว้ ขั้นตอน qpdf --check ยืนยันเพียงความสมบูรณ์เชิงโครงสร้างเท่านั้น โดย “the file may still contain errors that qpdf cannot detect” เป็นข้อแม้ของ qpdf เอง ที่ยกมาแบบคำต่อคำข้างต้น การตัดสินความสอดคล้องต่อมาตรฐาน (PDF/A-4, PDF/UA) เป็นหน้าที่ของตัวตรวจสอบอิสระ และเป็นพื้นผิวที่ต่างออกไป ดู รันการตรวจสอบมาตรฐานที่ระบุชื่อ สำหรับขอบเขตนั้น