Salta ai contenuti
getnextpdf.com

stabilità: Beta

Eseguire il rendering di una fattura end-to-end tramite REST

Portare una fattura da JSON a un PDF verificato localmente attraverso la superficie Representational State Transfer (REST) di NextPDF Connect, uno scambio sul canale alla volta. Questa ricetta invia un job di rendering a POST /api/v1/jobs, ripete l’invio con la stessa Idempotency-Key per mostrare il percorso a prova di duplicati, interroga GET /api/v1/jobs/{id}, scarica il PDF da GET /api/v1/jobs/{id}/result, controlla i byte con qpdf --check ed elimina il job completato.

Ogni risposta qui sotto è una cattura letterale da un deployment Connect di tier core reale (nextpdf/server sotto RoadRunner, associato a http://localhost:8080). L’unica sostituzione è la chiave API, mostrata come la variabile d’ambiente $NEXTPDF_CONNECT_TOKEN; identificatori di job, identificatori di richiesta, timestamp, header e byte del corpo sono esattamente ciò che il server ha restituito. Valori di header come Date e X-Request-Id differiranno, ovviamente, sul proprio deployment.

Questa ricetta gestisce un singolo documento, così da poter leggere ogni scambio per intero. Per molti documenti, concorrenza limitata e cicli di polling guidati da Retry-After, vedere Generare PDF in batch con monitoraggio dell’avanzamento, che usa la stessa superficie di job.

Il lato server è la distribuzione Connect standard:

Terminal window
composer require nextpdf/server

Il lato client di questa ricetta è curl più qpdf, così da poterla portare su qualsiasi client HTTP. Esportare prima i valori del proprio 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:

La superficie dei job asincroni separa l’invio dal recupero: si invia una richiesta di rendering, si riceve un record del job e si recupera il risultato quando il job raggiunge lo stato completed. La richiesta di rendering stessa è un array operations ordinato — gli stessi tipi di operazione (set_font, add_text, add_table, add_image, add_page) che sostengono le chiamate agli strumenti Connect su ogni trasporto — insieme a campi a livello di documento (page_size, orientation, title, author).

Due dettagli del contratto danno forma alla trascrizione che si sta per leggere:

  • Invio idempotente. Un invio con chiave Idempotency-Key restituisce 201 Created la prima volta e 200 OK con lo stesso record del job quando viene ripetuto, così un retry di rete non esegue mai il rendering due volte.
  • L’invio può essere già terminale. La release corrente elabora il job in modo sincrono prima di rispondere al POST, quindi la risposta all’invio può già riportare status: "completed" — come avviene qui sotto. Il contratto poll-until-terminal è la forma stabile dell’API: scrivere il ciclo di polling e accettare uno stato terminale a qualsiasi tentativo, incluso il primo.

Si può confermare ciò che il proprio deployment espone prima di inviare qualsiasi cosa: GET /api/v1/capabilities restituisce il catalogo delle operazioni raggiungibili dal tier della propria chiave API. Il deployment di tier core catturato qui elencava solo le operazioni core; il catalogo di riferimento è sempre la risposta del server in esecuzione, non questa pagina.

ScambioMetodo e percorsoStato catturato
Inviare il job di renderingPOST /api/v1/jobs201 Created
Ripetere lo stesso invioPOST /api/v1/jobs (stessa Idempotency-Key)200 OK
Interrogare il record del jobGET /api/v1/jobs/{id}200 OK
Scaricare il PDFGET /api/v1/jobs/{id}/result200 OK, application/pdf
Eliminare il job completatoDELETE /api/v1/jobs/{id}204 No Content

L’autenticazione è un bearer token su ogni richiesta /api/v1/*: Authorization: Bearer npk_live_{kid}_{secret}. Le risposte JSON di successo condividono l’envelope { "data": ..., "meta": ... }; i campi su cui si agisce risiedono sotto data.

Scrivere la richiesta di rendering in invoice.json. È un semplice elenco deterministico di operazioni — una riga di intestazione in grassetto, una riga di emissione e una tabella di voci:

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

I campi della fattura qui sono dati di esempio. La forma autorevole degli argomenti per ciascuna operazione è quella riportata dal proprio deployment — su MCP, tools/list restituisce lo schema di input completo per ogni tipo di operazione usato da questa richiesta.

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

Il server risponde 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"
}
}

Il job è già terminale in questa cattura — status è "completed" e result_url è presente — perché la release corrente esegue il rendering in modo sincrono prima di rispondere. Non farci affidamento: trattare la risposta all’invio come il primo risultato di polling e diramare in base a data.status come per qualsiasi altro polling.

Ripetere esattamente lo stesso comando — stessa Idempotency-Key, stesso corpo:

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

Il server restituisce 200 OK — non 201 — con lo stesso job_id, e non avviene un secondo rendering (confrontare meta.duration_ms con la prima risposta):

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

Questo polling mostra un record terminale, quindi non c’è alcun header Retry-After né il campo poll_url. Mentre un job è ancora pending o running, il server imposta Retry-After (un intervallo di 2 secondi) su ogni polling — rispettarlo invece di eseguire il polling in un ciclo serrato.

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

Il corpo è il binario del PDF — 3.663 byte in questa cattura, corrispondenti all’header Content-Length — ed è omesso qui. Viene scritto in invoice-inv-2026-0042.pdf.

Un 200 con Content-Type: application/pdf non è, di per sé, la prova che il corpo sia un PDF ben formato. Eseguire un controllo strutturale con qpdf:

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

Output catturato per il file scaricato sopra:

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

Le parole stesse di qpdf sono il confine onesto: questo è un controllo di sintassi e di stream, non una determinazione di conformità rispetto ad alcuno standard.

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

Dopo l’eliminazione, il record del job e il suo risultato memorizzato non esistono più; un GET successivo sul job restituisce 404.

  • Diramare in base a data.status, non al solo stato HTTP. Invio, ripetizione e polling restituiscono tutti 2xx con un record del job; lo stato del ciclo di vita risiede in data.status (pending, running, completed, failed, cancelled).
  • Una chiave ripetuta con un corpo diverso è un 409 Conflict. La ripetizione idempotente 200 avviene solo quando il corpo corrisponde all’invio originale. Non riutilizzare mai una chiave per contenuti diversi.
  • /result prima del completamento è un 409. Scaricare solo dopo che un polling mostra completed. Il 409 è una risposta normale da esaminare, non un guasto del trasporto — la stessa separazione tra trasporto e stato che ogni ricetta Connect segue (vedere Convenzioni delle ricette).
  • I job sono associati al proprietario. Un job inviato con una chiave API è invisibile a un’altra chiave: un GET cross-owner restituisce 404, non 403. Eseguire il polling con la credenziale con cui si è effettuato l’invio.
  • progress può essere assente. Il record catturato non contiene alcun campo progress perché il job era già terminale. Quando il server traccia l’avanzamento per un job non terminale, data.progress è un intero da 0 a 100; trattare un campo mancante come sconosciuto, non come zero.
  • Un job failed contiene data.error. Registrarlo; non reinviare alla cieca.

Un job di rendering costa un invio, al massimo una manciata di polling e un download. I valori meta.duration_ms catturati parlano chiaro: 63,31 ms per eseguire il rendering della fattura all’invio, 1,03 ms per la ripetizione idempotente che non ha svolto alcun lavoro e letture di stato sotto il millisecondo. Eseguire il polling alla cadenza Retry-After del server invece che in un ciclo serrato; la lettura di stato è economica ma non gratuita, e il rate limiter la conteggia nel budget (osservare X-Ratelimit-Remaining diminuire negli header catturati). Per i batch, limitare i job in corso invece di inviarli tutti in una volta — la ricetta batch implementa quel ciclo.

  • Mantenere il bearer token solo nell’header Authorization. Mai in una query string, in una riga di log o in un file committato. La trascrizione sopra sostituisce una variabile d’ambiente esattamente per questo motivo.
  • Validare i byte scaricati prima di considerarli attendibili. Il passaggio 5 fa parte del flusso, non è un extra facoltativo: verificare che la risposta sia un PDF (almeno l’header %PDF, qpdf --check per la struttura) prima di archiviarlo o inoltrarlo.
  • Eliminare i job completati che non servono più. Il passaggio 6 rimuove dal server il risultato memorizzato; altrimenti un job completato rimane scaricabile fino a quando la garbage collection dei job del server lo rimuove.
  • Usare una chiave con privilegi minimi. Questo flusso richiede una chiave di rendering di tier core e nient’altro.

Questa ricetta non avanza alcuna affermazione normativa sugli standard. Esercita gli endpoint REST dei job asincroni di Connect e legge i campi del record del job che il server definisce. Il passaggio qpdf --check conferma solo l’integrità strutturale — “the file may still contain errors that qpdf cannot detect” è la stessa avvertenza di qpdf, citata letteralmente sopra. Determinare la conformità a uno standard (PDF/A-4, PDF/UA) è compito di un validatore indipendente, ed è una superficie diversa — vedere Eseguire un controllo di standard nominato per quel confine.