Pular para o conteúdo
getnextpdf.com

estabilidade: Beta

Renderize uma fatura de ponta a ponta via REST

Leve uma fatura de JSON até um PDF verificado localmente, através da superfície Representational State Transfer (REST) do NextPDF Connect, uma troca de rede por vez. Esta receita envia um job de renderização para POST /api/v1/jobs, repete o envio com a mesma Idempotency-Key para mostrar o caminho seguro contra duplicação, consulta GET /api/v1/jobs/{id}, baixa o PDF de GET /api/v1/jobs/{id}/result, verifica os bytes com qpdf --check e exclui o job finalizado.

Cada resposta abaixo é uma captura literal de uma implantação Connect real de tier core (nextpdf/server sob RoadRunner, associada a http://localhost:8080). A única substituição é a API key, mostrada como a variável de ambiente $NEXTPDF_CONNECT_TOKEN; identificadores de job, identificadores de requisição, timestamps, cabeçalhos e bytes do corpo são exatamente o que o servidor retornou. Valores de cabeçalho como Date e X-Request-Id naturalmente serão diferentes na sua implantação.

Esta receita conduz um único documento para que você possa ler cada troca por completo. Para muitos documentos, concorrência limitada e laços de consulta guiados por Retry-After, consulte Gere PDFs em lote com rastreamento de progresso, que usa a mesma superfície de jobs.

O lado do servidor é a distribuição Connect padrão:

Terminal window
composer require nextpdf/server

O lado do cliente desta receita é curl mais qpdf, então você pode portá-la para qualquer cliente HTTP. Exporte primeiro os valores da sua implantação:

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

A superfície de jobs assíncronos separa o envio da recuperação: você envia uma requisição de renderização, recebe um registro de job e busca o resultado quando o job atinge completed. A própria requisição de renderização é um array operations ordenado — os mesmos tipos de operação (set_font, add_text, add_table, add_image, add_page) que sustentam as chamadas de ferramenta do Connect em todo transporte — mais campos no nível do documento (page_size, orientation, title, author).

Dois detalhes do contrato moldam a transcrição que você está prestes a ler:

  • Envio idempotente. Um envio identificado com Idempotency-Key retorna 201 Created na primeira vez e 200 OK com o mesmo registro de job quando repetido, de modo que uma nova tentativa de rede nunca renderiza duas vezes.
  • O envio pode já estar em estado terminal. A versão atual processa o job inline antes de responder ao POST, então a resposta de envio já pode carregar status: "completed" — como acontece abaixo. O contrato de consultar até o estado terminal é o formato estável da API: escreva o laço de consulta e aceite um estado terminal em qualquer tentativa, inclusive na primeira.

Você pode confirmar o que sua implantação expõe antes de enviar qualquer coisa: GET /api/v1/capabilities retorna o catálogo de operações que o tier da sua API key consegue alcançar. Na implantação de tier core capturada aqui, ele listou apenas as operações core; o catálogo oficial é sempre a resposta do próprio servidor em execução, não esta página.

TrocaMétodo e caminhoStatus capturado
Enviar o job de renderizaçãoPOST /api/v1/jobs201 Created
Repetir o mesmo envioPOST /api/v1/jobs (mesma Idempotency-Key)200 OK
Consultar o registro do jobGET /api/v1/jobs/{id}200 OK
Baixar o PDFGET /api/v1/jobs/{id}/result200 OK, application/pdf
Excluir o job finalizadoDELETE /api/v1/jobs/{id}204 No Content

A autenticação é um bearer token em toda requisição /api/v1/*: Authorization: Bearer npk_live_{kid}_{secret}. Respostas JSON bem-sucedidas compartilham o envelope { "data": ..., "meta": ... }; os campos sobre os quais você atua ficam em data.

Escreva a requisição de renderização em invoice.json. É uma lista simples e determinística de operações — uma linha de cabeçalho em negrito, uma linha de emissão e uma tabela de itens:

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

Os campos da fatura aqui são dados de exemplo. O formato autoritativo dos argumentos de cada operação é o que sua implantação relata — via MCP, tools/list retorna o schema de entrada completo para cada tipo de operação que esta requisição usa.

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

O servidor responde 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"
}
}

O job já está em estado terminal nesta captura — status é "completed" e result_url está presente — porque a versão atual renderiza inline antes de responder. Não dependa disso: trate a resposta de envio como o primeiro resultado de consulta e ramifique com base em data.status como em qualquer outra consulta.

Repita exatamente o mesmo comando — mesma Idempotency-Key, mesmo 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

O servidor retorna 200 OK — não 201 — com o mesmo job_id, e nenhuma segunda renderização acontece (compare meta.duration_ms com a primeira resposta):

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

Esta consulta mostra um registro terminal, portanto não há cabeçalho Retry-After nem campo poll_url. Enquanto um job ainda está pending ou running, o servidor define Retry-After (um intervalo de 2 segundos) em cada consulta — respeite-o em vez de consultar em um laço apertado.

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

O corpo é o binário do PDF — 3.663 bytes nesta captura, correspondendo ao cabeçalho Content-Length — e está omitido aqui. Ele é gravado em invoice-inv-2026-0042.pdf.

Um 200 com Content-Type: application/pdf não é, por si só, prova de que o corpo é um PDF bem-formado. Execute uma verificação estrutural com qpdf:

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

Saída capturada para o arquivo baixado acima:

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

As próprias palavras do qpdf são o limite honesto: esta é uma verificação de sintaxe e de streams, não uma determinação de conformidade com qualquer padrão.

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

Após a exclusão, o registro do job e seu resultado armazenado desaparecem; um GET subsequente no job retorna 404.

  • Ramifique com base em data.status, não apenas no status HTTP. Envio, repetição e consulta retornam todos 2xx com um registro de job; o estado do ciclo de vida fica em data.status (pending, running, completed, failed, cancelled).
  • Uma chave repetida com corpo diferente é um 409 Conflict. A repetição idempotente com 200 só acontece quando o corpo corresponde ao envio original. Nunca reutilize uma chave para conteúdo diferente.
  • /result antes da conclusão é um 409. Baixe somente depois que uma consulta mostrar completed. O 409 é uma resposta normal a ser inspecionada, não uma falha de transporte — a mesma separação entre transporte e status que toda receita do Connect segue (consulte Convenções de receita).
  • Jobs têm escopo de proprietário. Um job enviado sob uma API key é invisível para outra chave: um GET entre proprietários retorna 404, não 403. Consulte com a credencial usada no envio.
  • progress pode estar ausente. O registro capturado não carrega o campo progress porque o job já estava em estado terminal. Quando o servidor rastreia o progresso de um job não terminal, data.progress é um inteiro de 0 a 100; trate um campo ausente como desconhecido, não como zero.
  • Um job failed carrega data.error. Registre-o; não reenvie às cegas.

Um job de renderização custa um envio, no máximo um punhado de consultas e um download. Os valores meta.duration_ms capturados contam a história: 63,31 ms para renderizar a fatura no envio, 1,03 ms para a repetição idempotente que não fez trabalho algum e leituras de status abaixo de um milissegundo. Consulte na cadência de Retry-After do servidor em vez de um laço apertado; a leitura de status é barata, mas não gratuita, e o rate limiter a contabiliza (observe X-Ratelimit-Remaining decrescer nos cabeçalhos capturados). Para lotes, limite os jobs em andamento em vez de enviar tudo de uma vez — a receita de lote implementa esse laço.

  • Mantenha o bearer token apenas no cabeçalho Authorization. Nunca em uma query string, uma linha de log ou um arquivo commitado. A transcrição acima substitui uma variável de ambiente exatamente por esse motivo.
  • Valide os bytes baixados antes de confiar neles. A etapa 5 faz parte do fluxo, não é um extra opcional: verifique se a resposta é um PDF (o cabeçalho %PDF no mínimo, qpdf --check para a estrutura) antes de arquivá-la ou encaminhá-la.
  • Exclua os jobs finalizados de que você não precisa mais. A etapa 6 remove o resultado armazenado do servidor; caso contrário, um job concluído permanece disponível para download até que a coleta de lixo de jobs do servidor o remova.
  • Use uma chave de privilégio mínimo. Este fluxo precisa de uma chave de renderização de tier core e nada mais.

Esta receita não faz nenhuma afirmação normativa sobre padrões. Ela exercita os endpoints REST de jobs assíncronos do Connect e lê os campos do registro de job que o servidor define. A etapa qpdf --check confirma apenas a integridade estrutural — “the file may still contain errors that qpdf cannot detect” é a própria ressalva do qpdf, citada literalmente acima. Determinar a conformidade com um padrão (PDF/A-4, PDF/UA) é tarefa de um validador independente, e uma superfície diferente — consulte Execute uma verificação de padrão nomeado para esse limite.