تخطَّ إلى المحتوى
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⁩ فعلي من المستوى الأساسي (nextpdf/server تحت ⁨RoadRunner⁩، مربوطًا بـ http://localhost:8080). الاستبدال الوحيد هو مفتاح الواجهة، مُظهَرًا بوصفه متغير البيئة $NEXTPDF_CONNECT_TOKEN؛ أما مُعرِّفات المهام ومُعرِّفات الطلبات والطوابع الزمنية والترويسات وبايتات الجسم فهي بالضبط ما أعاده الخادم. وستختلف بالطبع قيم الترويسات مثل Date وX-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 مرتَّبة — أنواع العمليات نفسها (set_font، add_text، add_table، add_image، add_page) التي تدعم استدعاءات أدوات ⁨Connect⁩ عبر كل وسيلة نقل — إضافةً إلى حقول على مستوى المستند (page_size، orientation، title، author).

يُشكِّل تفصيلان في العقد النسخةَ المُلتقَطة التي أنت على وشك قراءتها:

  • إرسال آمن تجاه التكرار. يُعيد الإرسال المصحوب بترويسة Idempotency-Key رمز 201 Created في المرة الأولى، ويُعيد 200 OK مع سجل المهمة نفسه عند إعادة الإرسال، فلا تُنفِّذ إعادة المحاولة على الشبكة عملية عرض مرتين أبدًا.
  • قد يكون الإرسال نهائيًا بالفعل. يعالج الإصدار الحالي المهمة ضمن مسار الطلب قبل أن يردّ على POST، لذا يمكن أن تحمل استجابة الإرسال بالفعل status: "completed" — كما تفعل أدناه. عقد الاستطلاع حتى الحالة النهائية هو شكل الواجهة المستقر: اكتب حلقة الاستطلاع، واقبل حالة نهائية عند أي محاولة، بما في ذلك الأولى.

يمكنك تأكيد ما يكشفه نشرك قبل إرسال أي شيء: تُعيد GET /api/v1/capabilities كتالوج العمليات الذي يمكن لمستوى مفتاح واجهتك الوصول إليه. في النشر من المستوى الأساسي المُلتقَط هنا، أدرجت العمليات الأساسية فقط؛ والكتالوج المرجعي هو دائمًا استجابة الخادم قيد التشغيل نفسها، لا هذه الصفحة.

التبادلالطريقة والمسارالحالة المُلتقَطة
إرسال مهمة العرضPOST /api/v1/jobs201 Created
إعادة الإرسال نفسهPOST /api/v1/jobs (بترويسة Idempotency-Key نفسها)200 OK
استطلاع سجل المهمةGET /api/v1/jobs/{id}200 OK
تنزيل ملف ⁨PDF⁩GET /api/v1/jobs/{id}/result200 OK، application/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 مثل أي استطلاع آخر.

2. إعادة الإرسال (المسار الآمن تجاه التكرار)

قسم بعنوان «2. إعادة الإرسال (المسار الآمن تجاه التكرار)»

أعِد المحاولة بالأمر نفسه تمامًا — ترويسة 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 (فاصل زمني قدره ثانيتان) في كل استطلاع — احترِمه بدلًا من الاستطلاع في حلقة محكمة.

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.

5. التحقّق من البايتات المُنزَّلة محليًا

قسم بعنوان «5. التحقّق من البايتات المُنزَّلة محليًا»

إن 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. لا تحدث إعادة الإرسال الآمنة تجاه التكرار بالرمز 200 إلا حين يطابق الجسمُ الإرسالَ الأصلي. لا تُعِد أبدًا استخدام مفتاح لمحتوى مختلف.
  • /result قبل الاكتمال هو 409. نزِّل فقط بعد أن يُظهِر الاستطلاع completed. الرمز 409 استجابة عادية تُفحَص، لا فشل نقل — وهو الفصل نفسه بين النقل والحالة الذي تتبعه كل وصفة من وصفات ⁨Connect⁩ (راجِع اصطلاحات الوصفات).
  • المهام محصورة بالمالك. مهمة مُرسَلة بمفتاح واجهة واحد تكون غير مرئية لمفتاح آخر: GET عبر مالك مختلف يُعيد 404، لا 403. استطلِع ببيانات الاعتماد نفسها التي أرسلت بها.
  • قد يكون progress غائبًا. لا يحمل السجل المُلتقَط أي حقل progress لأن المهمة كانت نهائية بالفعل. وحين يتتبَّع الخادم التقدُّم لمهمة غير نهائية، يكون data.progress عددًا صحيحًا من 0 إلى 100؛ عامِل الحقل المفقود على أنه مجهول، لا صفر.
  • مهمة failed تحمل data.error. سجِّلها؛ ولا تُعِد الإرسال بشكل أعمى.

تُكلِّف مهمة عرض واحدة إرسالًا واحدًا، وبضعة استطلاعات على الأكثر، وتنزيلًا واحدًا. تروي قيم meta.duration_ms المُلتقَطة القصة: ⁨63.31⁩ مِلي ثانية لعرض الفاتورة عند الإرسال، و⁨1.03⁩ مِلي ثانية لإعادة الإرسال الآمنة تجاه التكرار التي لم تُنجِز أي عمل، وقراءات حالة دون المِلي ثانية. استطلِع بوتيرة Retry-After لدى الخادم بدلًا من حلقة محكمة؛ فقراءة الحالة زهيدة التكلفة لكنها ليست مجانية، ويرصد لها مقيِّد المعدل ميزانية (راقِب X-Ratelimit-Remaining وهو يتناقص في الترويسات المُلتقَطة). أما للدفعات، فقيِّد المهام قيد التنفيذ بدلًا من إرسال كل شيء دفعةً واحدة — وصفة المعالجة المُجمَّعة تُنفِّذ تلك الحلقة.

  • أبقِ الرمز الحامل في ترويسة Authorization فقط. لا تضعه أبدًا في سلسلة استعلام، أو سطر سجل، أو ملف مُودَع في المستودع. تستبدل النسخة المُلتقَطة أعلاه متغير بيئة لهذا السبب بالضبط.
  • تحقَّق من صحة البايتات المُنزَّلة قبل الوثوق بها. الخطوة 5 جزء من التدفق، لا إضافة اختيارية: تأكَّد أن الاستجابة ملف ⁨PDF⁩ (ترويسة %PDF كحدّ أدنى، وqpdf --check للبِنية) قبل أرشفتها أو إعادة توجيهها.
  • احذف المهام المكتملة التي لم تعد بحاجة إليها. تُزيل الخطوة 6 النتيجة المخزَّنة من الخادم؛ وإلا تبقى المهمة المكتملة قابلة للتنزيل حتى يُزيلها جامع نفايات المهام لدى الخادم.
  • استخدِم مفتاحًا بأقل امتياز. يحتاج هذا التدفق مفتاح عرض من المستوى الأساسي ولا شيء أكثر.

لا تطرح هذه الوصفة أي ادعاء معياري مُلزِم. إنها تستخدم نقاط نهاية ⁨REST⁩ للمهام غير المتزامنة في ⁨Connect⁩ وتقرأ حقول سجل المهمة التي يُعرِّفها الخادم. تؤكد خطوة qpdf --check السلامة البنيوية فقط — وعبارة ⁨“the file may still contain errors that qpdf cannot detect”⁩ هي تحذير ⁨qpdf⁩ نفسه، مُقتبَسًا حرفيًا أعلاه. أما تحديد المطابقة لمعيار (⁨PDF/A-4⁩، ⁨PDF/UA⁩) فهو مهمة مُدقِّق مستقل، وسطح مختلف — راجِع إجراء فحص لمعيار مُسمّى لأجل ذلك الحدّ.