Ga naar inhoud
getnextpdf.com

stabiliteit: Bèta

Een factuur end-to-end renderen via REST

Breng één factuur van JSON naar een lokaal geverifieerde PDF via het Representational State Transfer (REST)-oppervlak van NextPDF Connect, één netwerkuitwisseling tegelijk. Dit recipe dient een renderopdracht in bij POST /api/v1/jobs, herhaalt de indiening met dezelfde Idempotency-Key om de duplicaat-veilige route te tonen, pollt GET /api/v1/jobs/{id}, downloadt de PDF van GET /api/v1/jobs/{id}/result, controleert de bytes met qpdf --check en verwijdert de voltooide job.

Elke respons hieronder is een letterlijke vastlegging van een echte Connect-deployment op Core-niveau (nextpdf/server onder RoadRunner, gebonden aan http://localhost:8080). Het enige wat is vervangen, is de API key, weergegeven als de omgevingsvariabele $NEXTPDF_CONNECT_TOKEN; job-identifiers, verzoek-identifiers, tijdstempels, headers en body-bytes zijn precies wat de server retourneerde. Headerwaarden zoals Date en X-Request-Id verschillen uiteraard op jouw deployment.

Dit recipe verwerkt één enkel document zodat je elke uitwisseling volledig kunt lezen. Voor veel documenten, begrensde gelijktijdigheid en Retry-After-gestuurde poll-lussen, zie Batchgewijs PDF’s genereren met voortgangstracking, dat hetzelfde job-oppervlak gebruikt.

De serverkant is de standaard Connect-distributie:

Terminal window
composer require nextpdf/server

De clientkant van dit recipe is curl plus qpdf, zodat je het naar elke HTTP-client kunt overzetten. Exporteer eerst de waarden van jouw deployment:

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

Het async-job-oppervlak scheidt indiening van ophalen: je dient een renderverzoek in, ontvangt een job-record en haalt het resultaat op wanneer de job completed bereikt. Het renderverzoek zelf is een geordende operations-array — dezelfde operatietypes (set_font, add_text, add_table, add_image, add_page) die de Connect-toolaanroepen op elk transport ondersteunen — plus velden op documentniveau (page_size, orientation, title, author).

Twee contractdetails bepalen het transcript dat je zo gaat lezen:

  • Idempotente indiening. Een indiening met een Idempotency-Key retourneert de eerste keer 201 Created en bij herhaling 200 OK met hetzelfde job-record, zodat een netwerk-retry nooit twee keer rendert.
  • Een indiening kan al terminaal zijn. De huidige release verwerkt de job inline voordat hij de POST beantwoordt, dus de indieningsrespons kan al status: "completed" bevatten — zoals hieronder het geval is. Het poll-until-terminal-contract is de stabiele API-vorm: schrijf de poll-lus en accepteer een terminale status bij elke poging, inclusief de eerste.

Je kunt bevestigen wat jouw deployment blootstelt voordat je iets indient: GET /api/v1/capabilities retourneert de operatiecatalogus die het niveau van jouw API key kan bereiken. Op de hier vastgelegde deployment op Core-niveau vermeldde die alleen de Core-operaties; de gezaghebbende catalogus is altijd de eigen respons van de draaiende server, niet deze pagina.

UitwisselingMethode en padVastgelegde status
Renderopdracht indienenPOST /api/v1/jobs201 Created
Dezelfde indiening herhalenPOST /api/v1/jobs (zelfde Idempotency-Key)200 OK
Job-record pollenGET /api/v1/jobs/{id}200 OK
De PDF downloadenGET /api/v1/jobs/{id}/result200 OK, application/pdf
De voltooide job verwijderenDELETE /api/v1/jobs/{id}204 No Content

Authenticatie is een bearer-token bij elk /api/v1/*-verzoek: Authorization: Bearer npk_live_{kid}_{secret}. Succesvolle JSON-responses delen de envelop { "data": ..., "meta": ... }; de velden waarmee je werkt bevinden zich onder data.

Schrijf het renderverzoek naar invoice.json. Het is een eenvoudige, deterministische operatielijst — een vetgedrukte kopregel, een uitgifteregel en een tabel met regelitems:

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

De factuurvelden hier zijn voorbeelddata. De gezaghebbende argumentvorm voor elke operatie is wat jouw deployment rapporteert — via MCP retourneert tools/list het volledige input-schema voor elk operatietype dat dit verzoek gebruikt.

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

De server antwoordt met 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"
}
}

De job is in deze vastlegging al terminaal — status is "completed" en result_url is aanwezig — omdat de huidige release inline rendert voordat hij antwoordt. Vertrouw daar niet op: behandel de indieningsrespons als het eerste poll-resultaat en vertak op basis van data.status zoals bij elke andere poll.

Herhaal exact hetzelfde commando — zelfde Idempotency-Key, zelfde body:

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

De server retourneert 200 OK — niet 201 — met hetzelfde job_id, en er vindt geen tweede render plaats (vergelijk meta.duration_ms met de eerste respons):

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

Deze poll toont een terminaal record, dus er is geen Retry-After-header en geen poll_url-veld. Zolang een job nog pending of running is, stelt de server bij elke poll Retry-After in (een interval van 2 seconden) — respecteer die in plaats van in een strakke lus te pollen.

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

De body is de PDF-binary — 3.663 bytes in deze vastlegging, overeenkomend met de Content-Length-header — en is hier weggelaten. Hij wordt weggeschreven naar invoice-inv-2026-0042.pdf.

Een 200 met Content-Type: application/pdf is op zichzelf geen bewijs dat de body een goedgevormde PDF is. Voer een structurele controle uit met qpdf:

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

Vastgelegde uitvoer voor het hierboven gedownloade bestand:

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’s eigen formulering is de eerlijke grens: dit is een syntax- en streamcontrole, geen conformiteitsbepaling ten opzichte van enige standaard.

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

Na het verwijderen zijn het job-record en het opgeslagen resultaat weg; een volgende GET op de job retourneert 404.

  • Vertak op basis van data.status, niet op de HTTP-status alleen. Indienen, herhalen en pollen retourneren allemaal 2xx met een job-record; de levenscyclusstatus zit in data.status (pending, running, completed, failed, cancelled).
  • Een herhaalde key met een andere body levert een 409 Conflict op. De idempotente 200-herhaling gebeurt alleen wanneer de body overeenkomt met de oorspronkelijke indiening. Hergebruik een key nooit voor andere inhoud.
  • /result vóór voltooiing levert een 409 op. Download pas nadat een poll completed toont. De 409 is een normale respons om te inspecteren, geen transportstoring — dezelfde scheiding tussen transport en status die elk Connect-recipe volgt (zie Recipe-conventies).
  • Jobs zijn eigenaargebonden. Een job die onder één API key is ingediend, is onzichtbaar voor een andere key: een cross-owner-GET retourneert 404, niet 403. Poll met de credential waarmee je hebt ingediend.
  • progress kan ontbreken. Het vastgelegde record bevat geen progress-veld omdat de job al terminaal was. Wanneer de server de voortgang van een niet-terminale job bijhoudt, is data.progress een geheel getal van 0 tot 100; behandel een ontbrekend veld als onbekend, niet als nul.
  • Een failed job bevat data.error. Registreer die; dien niet blindelings opnieuw in.

Eén renderopdracht kost één indiening, hooguit een handvol polls en één download. De vastgelegde meta.duration_ms-waarden vertellen het verhaal: 63,31 ms om de factuur te renderen bij indiening, 1,03 ms voor de idempotente herhaling die geen werk deed, en status-leesbewerkingen onder de milliseconde. Poll op de Retry-After-cadans van de server in plaats van in een strakke lus; de status-leesbewerking is goedkoop maar niet gratis, en de rate limiter budgetteert die (zie X-Ratelimit-Remaining aftellen in de vastgelegde headers). Begrens voor batches de lopende jobs in plaats van alles tegelijk in te dienen — het batch-recipe implementeert die lus.

  • Houd het bearer-token uitsluitend in de Authorization-header. Nooit in een query string, een logregel of een gecommit bestand. Het bovenstaande transcript vervangt het door een omgevingsvariabele om precies die reden.
  • Valideer gedownloade bytes voordat je ze vertrouwt. Stap 5 hoort bij de flow, geen optionele extra: controleer of de respons een PDF is (minimaal de %PDF-header, qpdf --check voor de structuur) voordat je die archiveert of doorstuurt.
  • Verwijder voltooide jobs die je niet meer nodig hebt. Stap 6 verwijdert het opgeslagen resultaat van de server; een voltooide job blijft anders downloadbaar totdat de job garbage collection van de server hem opruimt.
  • Gebruik een key met minimale rechten. Deze flow heeft een render-key op Core-niveau nodig en niets meer.

Dit recipe doet geen normatieve standaardenclaim. Het spreekt de Connect async-job REST-endpoints aan en leest de job-recordvelden die de server definieert. De stap qpdf --check bevestigt alleen de structurele integriteit — “the file may still contain errors that qpdf cannot detect” is qpdf’s eigen voorbehoud, hierboven letterlijk geciteerd. Het bepalen van conformiteit aan een standaard (PDF/A-4, PDF/UA) is de taak van een onafhankelijke validator, en een ander oppervlak — zie Een controle op een benoemde standaard uitvoeren voor die grens.