stabilité: Bêta
Générer une facture de bout en bout via REST
En un coup d’œil
Section intitulée « En un coup d’œil »Mène une facture depuis un JSON jusqu’à un PDF vérifié localement, à travers
la surface Representational State Transfer (REST) de NextPDF Connect, un
échange réseau à la fois. Cette recette soumet un job de rendu à
POST /api/v1/jobs, rejoue la soumission sous la même Idempotency-Key
pour montrer le comportement sûr en cas de doublon, interroge
GET /api/v1/jobs/{id}, télécharge le PDF depuis
GET /api/v1/jobs/{id}/result, vérifie les octets avec qpdf --check et
supprime le job terminé.
Chaque réponse ci-dessous est une capture textuelle issue d’un véritable
déploiement Connect de niveau core (nextpdf/server sous RoadRunner, lié à
http://localhost:8080). La seule substitution est la clé d’API, présentée
sous forme de variable d’environnement $NEXTPDF_CONNECT_TOKEN ; les
identifiants de job, les identifiants de requête, les horodatages, les
en-têtes et les octets du corps sont exactement ce que le serveur a renvoyé.
Des valeurs d’en-tête telles que Date et X-Request-Id différeront bien
sûr sur ton déploiement.
Cette recette pilote un seul document afin que tu puisses lire chaque échange
dans son intégralité. Pour de nombreux documents, une concurrence bornée et
des boucles de sondage guidées par Retry-After, consulte
Générer des PDF par lots avec suivi de progression,
qui utilise la même surface de jobs.
Installation
Section intitulée « Installation »Côté serveur, il s’agit de la distribution Connect standard :
composer require nextpdf/serverLe côté client de cette recette repose sur curl et qpdf, de sorte que tu
peux le porter vers n’importe quel client HTTP. Exporte d’abord les valeurs
de ton déploiement :
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:Vue d’ensemble conceptuelle
Section intitulée « Vue d’ensemble conceptuelle »La surface des jobs asynchrones sépare la soumission de la récupération : tu
soumets une requête de rendu, tu reçois un enregistrement de job et tu
récupères le résultat lorsque le job atteint completed. La requête de
rendu elle-même est un tableau operations ordonné — les mêmes types
d’opérations (set_font, add_text, add_table, add_image, add_page)
qui sous-tendent les appels d’outils Connect sur chaque transport —
auxquels s’ajoutent des champs de niveau document (page_size,
orientation, title, author).
Deux détails du contrat façonnent la transcription que tu t’apprêtes à lire :
- Soumission idempotente. Une soumission identifiée par une
Idempotency-Keyrenvoie201 Createdla première fois et200 OKavec le même enregistrement de job lorsqu’elle est rejouée, de sorte qu’un réessai réseau ne déclenche jamais deux rendus. - La soumission peut déjà être terminale. La version actuelle traite le
job de façon synchrone avant de répondre au
POST, de sorte que la réponse de soumission peut déjà porterstatus: "completed"— comme c’est le cas ci-dessous. Le contrat « interroger jusqu’à l’état terminal » est la forme stable de l’API : écris la boucle de sondage et accepte un état terminal à n’importe quelle tentative, y compris la première.
Tu peux confirmer ce que ton déploiement expose avant de soumettre quoi que
ce soit : GET /api/v1/capabilities renvoie le catalogue d’opérations que
le niveau de ta clé d’API peut atteindre. Sur le déploiement de niveau core
capturé ici, il ne listait que les opérations core ; le catalogue de
référence est toujours la réponse du serveur en cours d’exécution, pas cette
page.
Surface de l’API
Section intitulée « Surface de l’API »| Échange | Méthode et chemin | Statut capturé |
|---|---|---|
| Soumettre le job de rendu | POST /api/v1/jobs | 201 Created |
| Rejouer la même soumission | POST /api/v1/jobs (même Idempotency-Key) | 200 OK |
| Interroger l’enregistrement du job | GET /api/v1/jobs/{id} | 200 OK |
| Télécharger le PDF | GET /api/v1/jobs/{id}/result | 200 OK, application/pdf |
| Supprimer le job terminé | DELETE /api/v1/jobs/{id} | 204 No Content |
L’authentification repose sur un jeton porteur pour chaque requête
/api/v1/* : Authorization: Bearer npk_live_{kid}_{secret}. Les réponses
JSON réussies partagent l’enveloppe { "data": ..., "meta": ... } ; les
champs sur lesquels tu agis se trouvent sous data.
La requête de facture
Section intitulée « La requête de facture »Écris la requête de rendu dans invoice.json. Il s’agit d’une liste
d’opérations simple et déterministe — une ligne d’en-tête en gras, une ligne
d’émission et un tableau de postes :
{ "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>" } ]}Les champs de facture présentés ici sont des données d’exemple. La forme
d’arguments faisant autorité pour chaque opération est celle que rapporte
ton déploiement — via MCP, tools/list renvoie le schéma d’entrée complet
pour chaque type d’opération que cette requête utilise.
Transcription de bout en bout
Section intitulée « Transcription de bout en bout »1. Soumettre le job de rendu
Section intitulée « 1. Soumettre le job de rendu »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.jsonLe serveur répond 201 Created :
HTTP/1.1 201 CreatedCache-Control: no-storeContent-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'Content-Type: application/json; charset=utf-8Referrer-Policy: no-referrerVary: Accept-EncodingX-Content-Type-Options: nosniffX-Frame-Options: DENYX-Powered-By: NextPDF ConnectX-Ratelimit-Remaining: 99X-Request-Id: 019f3fd6-c6ed-727c-958e-2fa4370ba97eDate: Wed, 08 Jul 2026 03:47:48 GMTTransfer-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" }}Le job est déjà terminal dans cette capture — status vaut "completed" et
result_url est présent — parce que la version actuelle effectue le rendu
de façon synchrone avant de répondre. Ne t’appuie pas sur ce comportement : traite la
réponse de soumission comme le premier résultat de sondage et base-toi sur
data.status comme pour n’importe quel autre sondage.
2. Rejouer la soumission (chemin idempotent)
Section intitulée « 2. Rejouer la soumission (chemin idempotent) »Réexécute exactement la même commande — même Idempotency-Key, même corps :
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.jsonLe serveur renvoie 200 OK — et non 201 — avec le même job_id, et aucun
second rendu ne se produit (compare meta.duration_ms avec la première
réponse) :
HTTP/1.1 200 OKCache-Control: no-storeContent-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'Content-Type: application/json; charset=utf-8Referrer-Policy: no-referrerVary: Accept-EncodingX-Content-Type-Options: nosniffX-Frame-Options: DENYX-Powered-By: NextPDF ConnectX-Ratelimit-Remaining: 99X-Request-Id: 019f3fd6-c756-7226-bc85-b255d75bd449Date: Wed, 08 Jul 2026 03:47:48 GMTTransfer-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" }}3. Interroger l’enregistrement du job
Section intitulée « 3. Interroger l’enregistrement du job »curl -sS -i "$NEXTPDF_CONNECT_URL/api/v1/jobs/job_9bd0808960f10eb568484acc" \ -H "Authorization: Bearer $NEXTPDF_CONNECT_TOKEN"HTTP/1.1 200 OKCache-Control: no-storeContent-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'Content-Type: application/json; charset=utf-8Referrer-Policy: no-referrerVary: Accept-EncodingX-Content-Type-Options: nosniffX-Frame-Options: DENYX-Powered-By: NextPDF ConnectX-Ratelimit-Remaining: 98X-Request-Id: 019f3fd7-0038-7131-b71e-4c69e637233bDate: Wed, 08 Jul 2026 03:48:02 GMTTransfer-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" }}Ce sondage montre un enregistrement terminal : il n’y a donc pas d’en-tête
Retry-After ni de champ poll_url. Tant qu’un job est encore pending ou
running, le serveur définit Retry-After (un intervalle de 2 secondes) à
chaque sondage — respecte-le au lieu d’interroger dans une boucle serrée.
4. Télécharger le PDF
Section intitulée « 4. Télécharger le PDF »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.pdfHTTP/1.1 200 OKCache-Control: no-storeContent-Disposition: attachment; filename="job-job_9bd0808960f10eb568484acc.pdf"Content-Length: 3663Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'Content-Type: application/pdfReferrer-Policy: no-referrerVary: Accept-EncodingX-Content-Type-Options: nosniffX-Frame-Options: DENYX-Powered-By: NextPDF ConnectX-Ratelimit-Remaining: 98X-Request-Id: 019f3fd7-0056-711e-acad-9f7ac5f21ba7Date: Wed, 08 Jul 2026 03:48:02 GMTLe corps est le binaire du PDF — 3 663 octets dans cette capture, ce qui
correspond à l’en-tête Content-Length — et il est omis ici. Il est écrit
dans invoice-inv-2026-0042.pdf.
5. Vérifier localement les octets téléchargés
Section intitulée « 5. Vérifier localement les octets téléchargés »Un 200 avec Content-Type: application/pdf ne prouve pas, à lui seul, que
le corps est un PDF bien formé. Effectue une vérification structurelle avec
qpdf :
qpdf --check invoice-inv-2026-0042.pdfSortie capturée pour le fichier téléchargé ci-dessus :
checking invoice-inv-2026-0042.pdfPDF Version: 2.0File is not encryptedFile is not linearizedNo syntax or stream encoding errors found; the file may still containerrors that qpdf cannot detectLa formulation de qpdf pose elle-même la limite en toute honnêteté : il s’agit d’une vérification de syntaxe et de flux, pas d’un constat de conformité à une quelconque norme.
6. Supprimer le job terminé
Section intitulée « 6. Supprimer le job terminé »curl -sS -i -X DELETE "$NEXTPDF_CONNECT_URL/api/v1/jobs/job_9bd0808960f10eb568484acc" \ -H "Authorization: Bearer $NEXTPDF_CONNECT_TOKEN"HTTP/1.1 204 No ContentCache-Control: no-storeContent-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'Content-Type: application/json; charset=utf-8Referrer-Policy: no-referrerVary: Accept-EncodingX-Content-Type-Options: nosniffX-Frame-Options: DENYX-Powered-By: NextPDF ConnectX-Ratelimit-Remaining: 97X-Request-Id: 019f3fd7-0084-722f-bf54-3dabead8aeeaDate: Wed, 08 Jul 2026 03:48:02 GMTAprès la suppression, l’enregistrement du job et son résultat stocké ont
disparu ; un GET ultérieur sur le job renvoie 404.
Cas limites et pièges
Section intitulée « Cas limites et pièges »- Base-toi sur
data.status, pas sur le seul statut HTTP. La soumission, le rejeu et le sondage renvoient tous un2xxavec un enregistrement de job ; l’état du cycle de vie se trouve dansdata.status(pending,running,completed,failed,cancelled). - Une clé rejouée avec un corps différent donne un
409 Conflict. Le rejeu idempotent200ne se produit que lorsque le corps correspond à la soumission d’origine. Ne réutilise jamais une clé pour un contenu différent. - Appeler
/resultavant l’achèvement donne un409. Ne télécharge qu’une fois qu’un sondage indiquecompleted. Le409est une réponse normale à inspecter, pas une défaillance du transport — c’est la même séparation entre transport et statut que suit chaque recette Connect (voir Conventions des recettes Connect). - Les jobs sont limités à leur propriétaire. Un job soumis avec une clé
d’API est invisible pour une autre clé : un
GETinter-propriétaires renvoie404, et non403. Interroge avec l’identité que tu as utilisée pour soumettre. progresspeut être absent. L’enregistrement capturé ne porte aucun champprogressparce que le job était déjà terminal. Lorsque le serveur suit la progression d’un job non terminal,data.progressest un entier de 0 à 100 ; traite un champ manquant comme inconnu, pas comme zéro.- Un job
failedporte undata.error. Consigne-le ; ne resoumets pas à l’aveugle.
Performance
Section intitulée « Performance »Un job de rendu coûte une soumission, tout au plus une poignée de sondages
et un téléchargement. Les valeurs meta.duration_ms capturées parlent
d’elles-mêmes : 63,31 ms pour effectuer le rendu de la facture lors de la
soumission, 1,03 ms pour le rejeu idempotent qui n’a effectué aucun travail,
et des lectures de statut inférieures à la milliseconde. Interroge à la
cadence Retry-After du serveur plutôt que dans une boucle serrée ; la
lecture de statut est peu coûteuse mais pas gratuite, et le limiteur de
débit la comptabilise (observe X-Ratelimit-Remaining décroître dans les
en-têtes capturés). Pour les lots, borne le nombre de jobs en vol au lieu de
tout soumettre d’un coup — la
recette de lots
implémente cette boucle.
Notes de sécurité
Section intitulée « Notes de sécurité »- Conserve le jeton porteur uniquement dans l’en-tête
Authorization. Jamais dans une chaîne de requête, une ligne de journal ou un fichier versionné. La transcription ci-dessus substitue une variable d’environnement précisément pour cette raison. - Valide les octets téléchargés avant de leur faire confiance. L’étape 5
fait partie du flux, ce n’est pas une étape facultative : vérifie que la
réponse est bien un PDF (au minimum l’en-tête
%PDF,qpdf --checkpour la structure) avant de l’archiver ou de le transmettre. - Supprime les jobs terminés dont tu n’as plus besoin. L’étape 6 retire le résultat stocké du serveur ; sinon, un job terminé reste téléchargeable jusqu’à ce que le ramasse-miettes de jobs du serveur le supprime.
- Utilise une clé au moindre privilège. Ce flux nécessite une clé de rendu de niveau core, et rien de plus.
Conformité
Section intitulée « Conformité »Cette recette ne formule aucune revendication normative. Il sollicite les points de
terminaison REST des jobs asynchrones de Connect et lit les champs
d’enregistrement de job que le serveur définit. L’étape qpdf --check
confirme uniquement l’intégrité structurelle — « the file may still contain
errors that qpdf cannot detect » est la propre mise en garde de qpdf, citée
textuellement ci-dessus. Déterminer la conformité à une norme (PDF/A-4,
PDF/UA) relève d’un validateur indépendant, et d’une autre surface — voir
Lancer un contrôle de conformité
pour cette frontière.
Voir aussi
Section intitulée « Voir aussi »- Générer des PDF par lots avec suivi de progression — la même surface de jobs pilotée comme un lot à concurrence bornée.
- Générer votre premier PDF — le plus petit rendu Connect.
- Piloter une session de document agentique via MCP — le même moteur, outil par outil, via le transport stdio de MCP.
- Conventions des recettes Connect — le contrat de transport, de niveau et de conformité que suit chaque recette Connect.
- Gestion des erreurs sensible aux exceptions via Connect — comment séparer les défaillances de transport des statuts non réussis.