Zum Inhalt springen
getnextpdf.com

Stabilität: Beta

Eine Rechnung durchgängig über REST rendern

Bringen Sie eine Rechnung von JSON zu einem lokal verifizierten PDF über die Representational-State-Transfer-Fläche (REST) von NextPDF Connect — einen Austausch über die Leitung nach dem anderen. Dieses Recipe reicht einen Render-Job an POST /api/v1/jobs ein, wiederholt die Einreichung unter demselben Idempotency-Key, um den duplikatsicheren Pfad zu zeigen, fragt GET /api/v1/jobs/{id} ab, lädt das PDF von GET /api/v1/jobs/{id}/result herunter, prüft die Bytes mit qpdf --check und löscht den fertigen Job.

Jede Antwort unten ist eine wortgetreue Erfassung aus einer echten Connect-Bereitstellung der Core-Stufe (nextpdf/server unter RoadRunner, gebunden an http://localhost:8080). Die einzige Ersetzung ist der API-Schlüssel, dargestellt als Umgebungsvariable $NEXTPDF_CONNECT_TOKEN; Job-Bezeichner, Anfrage-Bezeichner, Zeitstempel, Header und Body-Bytes sind exakt das, was der Server zurückgegeben hat. Header-Werte wie Date und X-Request-Id unterscheiden sich in Ihrer Bereitstellung selbstverständlich.

Dieses Recipe verarbeitet ein einzelnes Dokument, sodass Sie jeden Austausch vollständig lesen können. Für viele Dokumente, begrenzte Nebenläufigkeit und Retry-After-gesteuerte Poll-Schleifen siehe PDFs im Batch mit Fortschrittsverfolgung erzeugen, das dieselbe Job-Fläche nutzt.

Die Serverseite ist die Standard-Connect-Distribution:

Terminal-Fenster
composer require nextpdf/server

Die Clientseite dieses Recipes besteht aus curl plus qpdf, sodass Sie es auf jeden HTTP-Client portieren können. Exportieren Sie zuerst die Werte Ihrer Bereitstellung:

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

Die Fläche der asynchronen Jobs trennt die Einreichung vom Abruf: Sie reichen eine Render-Anfrage ein, erhalten einen Job-Datensatz und rufen das Ergebnis ab, sobald der Job completed erreicht. Die Render-Anfrage selbst ist ein geordnetes operations-Array — dieselben Operationstypen (set_font, add_text, add_table, add_image, add_page), die den Connect-Werkzeugaufrufen auf jedem Transport zugrunde liegen — sowie Felder auf Dokumentebene (page_size, orientation, title, author).

Zwei Vertragsdetails prägen das Transkript, das Sie gleich lesen werden:

  • Idempotente Einreichung. Eine mit Idempotency-Key versehene Einreichung liefert beim ersten Mal 201 Created und bei einer Wiederholung 200 OK mit demselben Job-Datensatz, sodass ein Netzwerk-Retry niemals zweimal rendert.
  • Die Einreichung kann bereits terminal sein. Das aktuelle Release verarbeitet den Job inline, bevor es den POST beantwortet, sodass die Einreichungsantwort bereits status: "completed" tragen kann — so wie unten. Der Vertrag, bis zum Endzustand abzufragen, ist die stabile API-Form: Schreiben Sie die Poll-Schleife und akzeptieren Sie einen Endzustand bei jedem Versuch, auch beim ersten.

Sie können bestätigen, was Ihre Bereitstellung unterstützt, bevor Sie irgendetwas einreichen: GET /api/v1/capabilities gibt den Operationskatalog zurück, den die Stufe Ihres API-Schlüssels erreichen kann. Auf der hier erfassten Core-Stufen-Bereitstellung waren nur die Core-Operationen aufgeführt; der maßgebliche Katalog ist immer die Antwort des laufenden Servers selbst, nicht diese Seite.

AustauschMethode und PfadErfasster Status
Den Render-Job einreichenPOST /api/v1/jobs201 Created
Dieselbe Einreichung wiederholenPOST /api/v1/jobs (gleicher Idempotency-Key)200 OK
Den Job-Datensatz abfragenGET /api/v1/jobs/{id}200 OK
Das PDF herunterladenGET /api/v1/jobs/{id}/result200 OK, application/pdf
Den fertigen Job löschenDELETE /api/v1/jobs/{id}204 No Content

Die Authentifizierung erfolgt über ein Bearer-Token bei jeder /api/v1/*-Anfrage: Authorization: Bearer npk_live_{kid}_{secret}. Erfolgreiche JSON-Antworten teilen sich den Envelope { "data": ..., "meta": ... }; die Felder, mit denen Sie arbeiten, liegen unter data.

Schreiben Sie die Render-Anfrage in invoice.json. Es handelt sich um eine schlichte, deterministische Operationsliste — eine fette Kopfzeile, eine Ausstellungszeile und eine Tabelle mit Einzelposten:

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

Die Rechnungsfelder hier sind Beispieldaten. Die maßgebliche Form der Argumente für jede Operation ist diejenige, die Ihre Bereitstellung meldet — über MCP gibt tools/list das vollständige Eingabeschema für jeden Operationstyp zurück, den diese Anfrage verwendet.

Terminal-Fenster
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

Der Server antwortet mit 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"
}
}

Der Job ist in dieser Erfassung bereits terminal — status ist "completed" und result_url ist vorhanden —, weil das aktuelle Release inline rendert, bevor es antwortet. Verlassen Sie sich nicht darauf: Behandeln Sie die Einreichungsantwort als erstes Abfrageergebnis und verzweigen Sie über data.status wie bei jeder anderen Abfrage.

2. Die Einreichung wiederholen (idempotenter Pfad)

Abschnitt betitelt „2. Die Einreichung wiederholen (idempotenter Pfad)“

Wiederholen Sie exakt denselben Befehl — gleicher Idempotency-Key, gleicher Body:

Terminal-Fenster
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

Der Server gibt 200 OK zurück — nicht 201 — mit demselben job_id, und es findet kein zweiter Render statt (vergleichen Sie meta.duration_ms mit der ersten Antwort):

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

Diese Abfrage zeigt einen terminalen Datensatz, daher gibt es keinen Retry-After-Header und kein poll_url-Feld. Solange ein Job noch pending oder running ist, setzt der Server bei jeder Abfrage Retry-After (ein Intervall von 2 Sekunden) — halten Sie sich daran, statt in einer engen Schleife abzufragen.

Terminal-Fenster
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

Der Body ist das binäre PDF — 3.663 Bytes in dieser Erfassung, passend zum Content-Length-Header — und wird hier ausgelassen. Er wird in invoice-inv-2026-0042.pdf geschrieben.

Ein 200 mit Content-Type: application/pdf ist für sich genommen kein Beweis, dass der Body ein wohlgeformtes PDF ist. Führen Sie eine Strukturprüfung mit qpdf durch:

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

Erfasste Ausgabe für die oben heruntergeladene Datei:

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

Die Formulierung von qpdf ist die ehrliche Grenze: Dies ist eine Syntax- und Stream-Prüfung, keine Konformitätsfeststellung gegenüber irgendeinem Standard.

Terminal-Fenster
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

Nach dem Löschen sind der Job-Datensatz und sein gespeichertes Ergebnis verschwunden; ein anschließender GET auf den Job gibt 404 zurück.

  • Verzweigen Sie über data.status, nicht allein über den HTTP-Status. Einreichung, Wiederholung und Abfrage geben alle 2xx mit einem Job-Datensatz zurück; der Lebenszyklusstatus liegt in data.status (pending, running, completed, failed, cancelled).
  • Ein wiederholter Schlüssel mit einem anderen Body ist ein 409 Conflict. Die idempotente 200-Wiederholung tritt nur ein, wenn der Body mit der ursprünglichen Einreichung übereinstimmt. Verwenden Sie einen Schlüssel niemals für unterschiedliche Inhalte erneut.
  • /result vor Abschluss ist ein 409. Laden Sie erst herunter, nachdem eine Abfrage completed zeigt. Der 409 ist eine normale, zu prüfende Antwort, kein Transportfehler — dieselbe Trennung zwischen Transport und Status, der jedes Connect-Recipe folgt (siehe Recipe-Konventionen).
  • Jobs sind eigentümergebunden. Ein unter einem API-Schlüssel eingereichter Job ist für einen anderen Schlüssel unsichtbar: Ein GET über Eigentümergrenzen hinweg gibt 404 zurück, nicht 403. Fragen Sie mit derselben Anmeldeinformation ab, mit der Sie eingereicht haben.
  • progress kann fehlen. Der erfasste Datensatz trägt kein progress-Feld, weil der Job bereits terminal war. Wenn der Server den Fortschritt für einen nicht-terminalen Job verfolgt, ist data.progress eine Ganzzahl von 0 bis 100; behandeln Sie ein fehlendes Feld als unbekannt, nicht als null.
  • Ein failed-Job trägt data.error. Halten Sie es fest; reichen Sie nicht blind erneut ein.

Ein Render-Job kostet eine Einreichung, höchstens eine Handvoll Abfragen und einen Download. Die erfassten meta.duration_ms-Werte erzählen die Geschichte: 63,31 ms, um die Rechnung bei der Einreichung zu rendern, 1,03 ms für die idempotente Wiederholung, die keine Arbeit verrichtete, und Statusabfragen im Submillisekundenbereich. Fragen Sie im Retry-After-Takt des Servers ab statt in einer engen Schleife; die Statusabfrage ist günstig, aber nicht kostenlos, und der Ratenbegrenzer budgetiert sie (beobachten Sie, wie X-Ratelimit-Remaining in den erfassten Headern herunterzählt). Begrenzen Sie bei Batches die in Bearbeitung befindlichen Jobs, statt alles auf einmal einzureichen — das Batch-Recipe implementiert diese Schleife.

  • Halten Sie das Bearer-Token ausschließlich im Authorization-Header. Niemals in einem Query-String, einer Log-Zeile oder einer committeten Datei. Das Transkript oben ersetzt es genau aus diesem Grund durch eine Umgebungsvariable.
  • Validieren Sie heruntergeladene Bytes, bevor Sie ihnen vertrauen. Schritt 5 ist Teil des Ablaufs, kein optionaler Zusatz: Prüfen Sie, dass die Antwort ein PDF ist (mindestens %PDF-Header, qpdf --check für die Struktur), bevor Sie es archivieren oder weiterleiten.
  • Löschen Sie fertige Jobs, die Sie nicht mehr benötigen. Schritt 6 entfernt das gespeicherte Ergebnis vom Server; ein abgeschlossener Job bleibt andernfalls herunterladbar, bis die Job-Garbage-Collection des Servers ihn entfernt.
  • Verwenden Sie einen Schlüssel mit minimalen Rechten. Dieser Ablauf benötigt einen Render-Schlüssel der Core-Stufe und nichts weiter.

Dieses Recipe erhebt keinen normativen Standards-Anspruch. Es nutzt die REST-Endpunkte der asynchronen Jobs von Connect und liest die Felder des Job-Datensatzes, die der Server definiert. Der Schritt qpdf --check bestätigt nur die strukturelle Integrität — „the file may still contain errors that qpdf cannot detect“ ist qpdfs eigener Vorbehalt, oben wortgetreu zitiert. Die Konformität gegenüber einem Standard (PDF/A-4, PDF/UA) festzustellen, ist Aufgabe eines unabhängigen Prüfwerkzeugs und eine andere Fläche — siehe Eine Prüfung gegen einen benannten Standard ausführen für diese Grenze.