콘텐츠로 이동
getnextpdf.com

안정성: 베타

REST로 인보이스를 처음부터 끝까지 렌더링하기

인보이스 하나를 JSON에서 시작해 NextPDF Connect의 Representational State Transfer (REST) 표면을 통해 로컬에서 검증된 PDF까지, 한 번에 하나의 와이어 교환씩 따라갑니다. 이 레시피는 렌더링 작업을 POST /api/v1/jobs에 제출하고, 동일한 Idempotency-Key로 제출을 재전송해 중복 안전 경로를 보여주며, GET /api/v1/jobs/{id}를 폴링하고, GET /api/v1/jobs/{id}/result에서 PDF를 다운로드한 뒤, qpdf --check로 바이트를 확인하고, 완료된 작업을 삭제합니다.

아래의 모든 응답은 실제 core 등급 Connect 배포(RoadRunner 위에서 실행되며 http://localhost:8080에 바인딩된 nextpdf/server)에서 그대로 캡처한 것입니다. 유일하게 치환된 값은 $NEXTPDF_CONNECT_TOKEN 환경 변수로 표시된 API 키뿐이며, 작업 식별자, 요청 식별자, 타임스탬프, 헤더, 본문 바이트는 모두 서버가 반환한 그대로입니다. DateX-Request-Id 같은 헤더 값은 당연히 여러분의 배포에서는 다를 것입니다.

이 레시피는 단일 문서를 다루므로 각 교환을 전부 읽어볼 수 있습니다. 다수의 문서, 제한된 동시성, Retry-After 기반 폴링 루프에 대해서는 동일한 작업 표면을 사용하는 진행 상황 추적과 함께 PDF 일괄 생성하기를 참고하십시오.

서버 측은 표준 Connect 배포판입니다:

Terminal window
composer require nextpdf/server

이 레시피의 클라이언트 측은 curlqpdf이므로 어떤 HTTP 클라이언트로도 이식할 수 있습니다. 먼저 여러분의 배포 값을 내보내십시오:

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

비동기 작업 표면은 제출과 조회를 분리합니다. 렌더링 요청을 제출하면 작업 레코드를 받고, 작업이 completed에 도달하면 결과를 가져옵니다. 렌더링 요청 자체는 순서가 있는 operations 배열이며 — 모든 전송 방식에서 Connect 도구 호출을 뒷받침하는 동일한 작업 유형(set_font, add_text, add_table, add_image, add_page) — 여기에 문서 수준 필드(page_size, orientation, title, author)가 더해집니다.

여러분이 읽게 될 트랜스크립트를 좌우하는 두 가지 계약 세부 사항이 있습니다:

  • 멱등 제출. Idempotency-Key가 지정된 제출은 처음에는 201 Created를 반환하고, 재전송하면 동일한 작업 레코드와 함께 200 OK를 반환하므로, 네트워크 재시도가 두 번 렌더링하는 일은 결코 없습니다.
  • 제출이 이미 종료 상태일 수 있습니다. 현재 릴리스는 POST에 응답하기 전에 작업을 인라인으로 처리하므로, 제출 응답이 이미 status: "completed"를 담고 있을 수 있습니다 — 아래에서 실제로 그렇습니다. 종료 상태까지 폴링하는(poll-until-terminal) 계약이 안정적인 API 형태입니다. 폴링 루프를 작성하되, 첫 번째 시도를 포함한 어떤 시도에서든 종료 상태를 받아들이십시오.

무언가를 제출하기 전에 여러분의 배포가 무엇을 노출하는지 확인할 수 있습니다. GET /api/v1/capabilities는 여러분 API 키의 등급이 접근할 수 있는 작업 카탈로그를 반환합니다. 여기서 캡처한 core 등급 배포에서는 core 작업만 나열되었습니다. 기준이 되는 카탈로그는 언제나 이 페이지가 아니라 실행 중인 서버 자체의 응답입니다.

교환메서드와 경로캡처된 상태
렌더링 작업 제출POST /api/v1/jobs201 Created
동일한 제출 재전송POST /api/v1/jobs (동일한 Idempotency-Key)200 OK
작업 레코드 폴링GET /api/v1/jobs/{id}200 OK
PDF 다운로드GET /api/v1/jobs/{id}/result200 OK, application/pdf
완료된 작업 삭제DELETE /api/v1/jobs/{id}204 No Content

인증은 모든 /api/v1/* 요청에 대한 베어러 토큰입니다: Authorization: Bearer npk_live_{kid}_{secret}. 성공한 JSON 응답은 { "data": ..., "meta": ... } 봉투를 공유하며, 여러분이 다루게 될 필드는 data 아래에 있습니다.

렌더링 요청을 invoice.json에 작성합니다. 이것은 평범하고 결정론적인 작업 목록입니다 — 굵은 헤더 줄, 발행 줄, 그리고 항목별 표입니다:

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

여기의 인보이스 필드는 샘플 데이터입니다. 각 작업의 기준이 되는 인수 형태는 여러분의 배포가 보고하는 것입니다 — MCP에서는 tools/list가 이 요청이 사용하는 모든 작업 유형에 대한 전체 입력 스키마를 반환합니다.

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

서버는 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"
}
}

이 캡처에서 작업은 이미 종료 상태입니다 — status"completed"이고 result_url이 존재합니다 — 현재 릴리스가 응답하기 전에 인라인으로 렌더링하기 때문입니다. 여기에 의존하지 마십시오. 제출 응답을 첫 번째 폴링 결과로 취급하고, 다른 폴링과 마찬가지로 data.status에 따라 분기하십시오.

완전히 동일한 명령을 다시 실행합니다 — 동일한 Idempotency-Key, 동일한 본문:

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

서버는 동일한 job_id와 함께 201이 아닌 200 OK를 반환하며, 두 번째 렌더링은 일어나지 않습니다(meta.duration_ms를 첫 번째 응답과 비교해 보십시오):

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

이 폴링은 종료된 레코드를 보여주므로 Retry-After 헤더도, poll_url 필드도 없습니다. 작업이 아직 pending 또는 running 상태인 동안에는 서버가 매 폴링마다 Retry-After(2초 간격)를 설정합니다 — 촘촘한 루프로 폴링하는 대신 이 값을 준수하십시오.

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

본문은 PDF 바이너리이며 — 이 캡처에서는 Content-Length 헤더와 일치하는 3,663바이트입니다 — 여기서는 생략되었습니다. 이 데이터는 invoice-inv-2026-0042.pdf에 기록됩니다.

5. 다운로드한 바이트를 로컬에서 검증하기

섹션 제목: “5. 다운로드한 바이트를 로컬에서 검증하기”

Content-Type: application/pdf를 동반한 200이라고 해서 그 자체만으로 본문이 올바른 형식의 PDF라는 증거가 되지는 않습니다. qpdf로 구조 검사를 실행하십시오:

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

위에서 다운로드한 파일에 대한 캡처된 출력입니다:

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 자체의 표현이 정직한 경계선입니다. 이것은 구문과 스트림 검사이지, 어떤 표준에 대한 적합성 판정이 아닙니다.

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

삭제 후에는 작업 레코드와 저장된 결과가 사라집니다. 그 작업에 대한 이후의 GET404를 반환합니다.

  • HTTP 상태만이 아니라 data.status에 따라 분기하십시오. 제출, 재전송, 폴링은 모두 작업 레코드와 함께 2xx를 반환합니다. 수명 주기 상태는 data.status(pending, running, completed, failed, cancelled)에 담겨 있습니다.
  • 다른 본문으로 재전송된 키는 409 Conflict입니다. 멱등한 200 재전송은 본문이 원래 제출과 일치할 때만 발생합니다. 서로 다른 콘텐츠에 대해 키를 재사용하지 마십시오.
  • 완료 전 /result409입니다. 폴링이 completed를 보여준 후에만 다운로드하십시오. 이 409는 전송 실패가 아니라 확인해야 할 정상 응답입니다 — 모든 Connect 레시피가 따르는 동일한 전송 대 상태 분리입니다(레시피 규약 참고).
  • 작업은 소유자 범위로 한정됩니다. 하나의 API 키로 제출된 작업은 다른 키에는 보이지 않습니다. 소유자가 다른 GET403이 아니라 404를 반환합니다. 제출할 때 사용한 자격 증명으로 폴링하십시오.
  • progress가 없을 수 있습니다. 캡처된 레코드에는 작업이 이미 종료 상태였으므로 progress 필드가 없습니다. 서버가 종료되지 않은 작업의 진행 상황을 추적할 때 data.progress는 0에서 100 사이의 정수입니다. 필드가 없으면 0이 아니라 알 수 없음으로 취급하십시오.
  • failed 상태의 작업은 data.error를 담고 있습니다. 이를 기록하십시오. 무작정 재제출하지 마십시오.

하나의 렌더링 작업은 한 번의 제출, 많아야 몇 차례의 폴링, 그리고 한 번의 다운로드로 이루어집니다. 캡처된 meta.duration_ms 값이 그 전말을 말해 줍니다. 제출 시 인보이스를 렌더링하는 데 63.31 ms, 아무 작업도 하지 않은 멱등 재전송에 1.03 ms, 그리고 밀리초 미만의 상태 읽기입니다. 촘촘한 루프 대신 서버의 Retry-After 주기에 맞춰 폴링하십시오. 상태 읽기는 저렴하지만 공짜는 아니며, 속도 제한기가 이를 예산으로 관리합니다(캡처된 헤더에서 X-Ratelimit-Remaining이 줄어드는 것을 지켜보십시오). 일괄 처리의 경우 모든 것을 한 번에 제출하는 대신 진행 중인 작업 수를 제한하십시오 — 일괄 처리 레시피가 그 루프를 구현합니다.

  • 베어러 토큰은 Authorization 헤더에만 두십시오. 쿼리 문자열, 로그 줄, 커밋된 파일에는 절대 넣지 마십시오. 위 트랜스크립트가 환경 변수로 치환한 이유가 바로 그것입니다.
  • 다운로드한 바이트를 신뢰하기 전에 검증하십시오. 5단계는 선택적 추가 작업이 아니라 흐름의 일부입니다. 보관하거나 전달하기 전에 응답이 PDF인지 확인하십시오(최소한 %PDF 헤더, 구조에 대해서는 qpdf --check).
  • 더 이상 필요하지 않은 완료된 작업을 삭제하십시오. 6단계는 저장된 결과를 서버에서 제거합니다. 그렇지 않으면 완료된 작업은 서버의 작업 가비지 컬렉션이 제거할 때까지 다운로드 가능한 상태로 남아 있습니다.
  • 최소 권한 키를 사용하십시오. 이 흐름에는 core 등급 렌더링 키만 있으면 되고, 그 이상은 필요하지 않습니다.

이 레시피는 어떠한 규범적 표준 주장도 하지 않습니다. 이 레시피는 Connect 비동기 작업 REST 엔드포인트를 실행하고 서버가 정의하는 작업 레코드 필드를 읽습니다. qpdf --check 단계는 구조적 무결성만 확인합니다 — “파일에는 여전히 qpdf가 감지할 수 없는 오류가 있을 수 있습니다”라는 문구는 위에서 그대로 인용한 qpdf 자체의 주의 사항입니다. 표준(PDF/A-4, PDF/UA)에 대한 적합성을 판정하는 것은 독립적인 검증기의 몫이며, 다른 표면의 일입니다 — 그 경계에 대해서는 명명된 표준 검사 실행하기를 참고하십시오.