Przejdź do głównej zawartości
getnextpdf.com

stabilność: Beta

Renderowanie faktury od początku do końca przez REST

Przeprowadź jedną fakturę z formatu JSON do zweryfikowanego lokalnie pliku PDF przez powierzchnię Representational State Transfer (REST) NextPDF Connect, jedna wymiana po drugiej. Ten przepis przesyła zadanie renderowania do POST /api/v1/jobs, powtarza przesłanie pod tym samym kluczem Idempotency-Key, aby pokazać ścieżkę bezpieczną wobec duplikatów, odpytuje GET /api/v1/jobs/{id}, pobiera plik PDF z GET /api/v1/jobs/{id}/result, sprawdza bajty za pomocą qpdf --check i usuwa ukończone zadanie.

Każda odpowiedź poniżej to dosłowny zapis z rzeczywistego wdrożenia Connect w edycji core (nextpdf/server pod RoadRunner, powiązany z http://localhost:8080). Jedynym podstawieniem jest klucz API, przedstawiony jako zmienna środowiskowa $NEXTPDF_CONNECT_TOKEN; identyfikatory zadań, identyfikatory żądań, znaczniki czasu, nagłówki i bajty ciała są dokładnie tym, co zwrócił serwer. Wartości nagłówków, takie jak Date i X-Request-Id, będą oczywiście inne w Twoim wdrożeniu.

Ten przepis obsługuje pojedynczy dokument, aby można było przeczytać każdą wymianę w całości. W przypadku wielu dokumentów, ograniczonej współbieżności oraz pętli odpytywania sterowanych nagłówkiem Retry-After zobacz Wsadowe generowanie plików PDF ze śledzeniem postępu, które wykorzystuje tę samą powierzchnię zadań.

Strona serwera to standardowa dystrybucja Connect:

Okno terminala
composer require nextpdf/server

Stroną kliencką tego przepisu jest curl oraz qpdf, więc można go przenieść do dowolnego klienta HTTP. Najpierw wyeksportuj wartości swojego wdrożenia:

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

Asynchroniczna powierzchnia zadań oddziela przesłanie od pobrania: przesyłasz żądanie renderowania, otrzymujesz rekord zadania i pobierasz wynik, gdy zadanie osiągnie stan completed. Samo żądanie renderowania to uporządkowana tablica operations — te same typy operacji (set_font, add_text, add_table, add_image, add_page), które stoją za wywołaniami narzędzi Connect w każdym transporcie — wraz z polami na poziomie dokumentu (page_size, orientation, title, author).

Dwa szczegóły kontraktu kształtują zapis, który za chwilę przeczytasz:

  • Idempotentne przesłanie. Przesłanie oznaczone kluczem Idempotency-Key zwraca 201 Created za pierwszym razem oraz 200 OK z tym samym rekordem zadania przy ponowieniu, więc ponowienie sieciowe nigdy nie renderuje dwukrotnie.
  • Przesłanie może być już w stanie końcowym. Bieżące wydanie przetwarza zadanie w trybie inline przed odpowiedzią na POST, więc odpowiedź na przesłanie może już zawierać status: "completed" — jak poniżej. Kontrakt odpytywania aż do stanu końcowego to stabilny kształt API: napisz pętlę odpytywania i zaakceptuj stan końcowy przy każdej próbie, w tym pierwszej.

Możesz potwierdzić, co udostępnia Twoje wdrożenie, zanim cokolwiek prześlesz: GET /api/v1/capabilities zwraca katalog operacji, do których ma dostęp poziom Twojego klucza API. W przechwyconym tutaj wdrożeniu w edycji core wymieniono wyłącznie operacje core; katalogiem wiążącym jest zawsze własna odpowiedź działającego serwera, a nie ta strona.

WymianaMetoda i ścieżkaPrzechwycony status
Przesłanie zadania renderowaniaPOST /api/v1/jobs201 Created
Ponowienie tego samego przesłaniaPOST /api/v1/jobs (ten sam Idempotency-Key)200 OK
Odpytanie rekordu zadaniaGET /api/v1/jobs/{id}200 OK
Pobranie pliku PDFGET /api/v1/jobs/{id}/result200 OK, application/pdf
Usunięcie ukończonego zadaniaDELETE /api/v1/jobs/{id}204 No Content

Uwierzytelnianie to token bearer w każdym żądaniu /api/v1/*: Authorization: Bearer npk_live_{kid}_{secret}. Pomyślne odpowiedzi JSON współdzielą kopertę { "data": ..., "meta": ... }; pola, na których działasz, znajdują się pod data.

Zapisz żądanie renderowania do pliku invoice.json. To zwykła, deterministyczna lista operacji — pogrubiony wiersz nagłówka, wiersz wystawienia oraz tabela pozycji:

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

Pola faktury są tutaj przykładowymi danymi. Wiążącym kształtem argumentów dla każdej operacji jest ten zgłaszany przez Twoje wdrożenie — przez MCP tools/list zwraca pełny schemat wejściowy dla każdego typu operacji użytego w tym żądaniu.

Okno terminala
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

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

Zadanie jest już w stanie końcowym w tym zapisie — status to "completed", a result_url jest obecny — ponieważ bieżące wydanie renderuje w trybie inline przed odpowiedzią. Nie polegaj na tym: potraktuj odpowiedź na przesłanie jako pierwszy wynik odpytywania i rozgałęziaj logikę według data.status jak przy każdym innym odpytywaniu.

Powtórz dokładnie to samo polecenie — ten sam Idempotency-Key, to samo ciało:

Okno terminala
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

Serwer zwraca 200 OK — nie 201 — z tym samym job_id, a drugie renderowanie się nie odbywa (porównaj meta.duration_ms z pierwszą odpowiedzią):

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

To odpytanie pokazuje rekord w stanie końcowym, więc nie ma nagłówka Retry-After ani pola poll_url. Dopóki zadanie jest nadal w stanie pending lub running, serwer ustawia Retry-After (odstęp 2-sekundowy) przy każdym odpytaniu — respektuj go zamiast odpytywać w ciasnej pętli.

Okno terminala
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

Ciało to plik binarny PDF — 3663 bajty w tym zapisie, zgodnie z nagłówkiem Content-Length — i zostało tutaj pominięte. Jest zapisywane do invoice-inv-2026-0042.pdf.

200 z Content-Type: application/pdf samo w sobie nie jest dowodem, że ciało jest poprawnie sformułowanym plikiem PDF. Uruchom kontrolę strukturalną za pomocą qpdf:

Okno terminala
qpdf --check invoice-inv-2026-0042.pdf

Przechwycone dane wyjściowe dla pliku pobranego powyżej:

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

Własne sformułowanie qpdf to uczciwa granica: to kontrola składni i strumieni, a nie ustalenie zgodności z jakimkolwiek standardem.

Okno terminala
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

Po usunięciu rekord zadania i jego zapisany wynik znikają; kolejne GET dotyczące zadania zwraca 404.

  • Rozgałęziaj logikę według data.status, a nie wyłącznie według statusu HTTP. Przesłanie, ponowienie i odpytanie zwracają 2xx z rekordem zadania; stan cyklu życia znajduje się w data.status (pending, running, completed, failed, cancelled).
  • Ponowiony klucz z innym ciałem to 409 Conflict. Idempotentne ponowienie 200 następuje tylko wtedy, gdy ciało odpowiada oryginalnemu przesłaniu. Nigdy nie używaj ponownie klucza dla innej zawartości.
  • /result przed ukończeniem to 409. Pobieraj dopiero po tym, jak odpytanie pokaże completed. 409 to normalna odpowiedź do zbadania, a nie awaria transportu — to samo oddzielenie transportu od statusu, którym kieruje się każdy przepis Connect (zobacz Konwencje przepisów).
  • Zadania są ograniczone do właściciela. Zadanie przesłane pod jednym kluczem API jest niewidoczne dla innego klucza: GET między właścicielami zwraca 404, a nie 403. Odpytuj z użyciem poświadczenia, którym przesłałeś.
  • progress może być nieobecne. Przechwycony rekord nie zawiera pola progress, ponieważ zadanie było już w stanie końcowym. Gdy serwer śledzi postęp zadania, które nie jest w stanie końcowym, data.progress to liczba całkowita od 0 do 100; potraktuj brakujące pole jako nieznane, a nie zerowe.
  • Zadanie w stanie failed zawiera data.error. Zarejestruj to; nie przesyłaj ponownie na oślep.

Jedno zadanie renderowania to jedno przesłanie, co najwyżej garść odpytań i jedno pobranie. Przechwycone wartości meta.duration_ms opowiadają tę historię: 63,31 ms na wyrenderowanie faktury przy przesłaniu, 1,03 ms na idempotentne ponowienie, które nie wykonało żadnej pracy, oraz odczyty statusu poniżej milisekundy. Odpytuj w rytmie Retry-After serwera, a nie w ciasnej pętli; odczyt statusu jest tani, ale nie darmowy, a ogranicznik szybkości wlicza go do budżetu (obserwuj, jak X-Ratelimit-Remaining maleje w przechwyconych nagłówkach). W przypadku partii ograniczaj liczbę zadań w toku, zamiast przesyłać wszystko naraz — przepis wsadowy implementuje tę pętlę.

  • Przechowuj token bearer wyłącznie w nagłówku Authorization. Nigdy w ciągu zapytania, wierszu dziennika ani w zatwierdzonym pliku. Zapis powyżej podstawia zmienną środowiskową właśnie z tego powodu.
  • Weryfikuj pobrane bajty, zanim im zaufasz. Krok 5 jest częścią przepływu, a nie opcjonalnym dodatkiem: sprawdź, czy odpowiedź jest plikiem PDF (co najmniej nagłówek %PDF, qpdf --check dla struktury), zanim ją zarchiwizujesz lub przekażesz dalej.
  • Usuwaj ukończone zadania, których już nie potrzebujesz. Krok 6 usuwa zapisany wynik z serwera; w przeciwnym razie ukończone zadanie pozostaje możliwe do pobrania, dopóki nie usunie go mechanizm odśmiecania zadań serwera.
  • Używaj klucza o najmniejszych uprawnieniach. Ten przepływ potrzebuje klucza renderującego w edycji core i niczego więcej.

Ten przepis nie formułuje żadnego normatywnego twierdzenia dotyczącego standardów. Wykorzystuje asynchroniczne punkty końcowe REST zadań Connect i odczytuje pola rekordu zadania definiowane przez serwer. Krok qpdf --check potwierdza wyłącznie integralność strukturalną — „the file may still contain errors that qpdf cannot detect” to własne zastrzeżenie qpdf, przytoczone dosłownie powyżej. Ustalenie zgodności ze standardem (PDF/A-4, PDF/UA) należy do niezależnego walidatora i innej powierzchni — zobacz Uruchom kontrolę nazwanego standardu w kwestii tej granicy.