estabilidad: Beta
Renderizar una factura de principio a fin sobre REST
De un vistazo
Sección titulada «De un vistazo»Llevar una factura desde JSON hasta un PDF verificado localmente a través
de la superficie REST (Representational State Transfer) de NextPDF
Connect, un intercambio de red a la vez. Esta receta envía un trabajo de
renderizado a POST /api/v1/jobs, repite el envío con la misma
Idempotency-Key para mostrar la ruta segura ante duplicados, sondea
GET /api/v1/jobs/{id}, descarga el PDF desde
GET /api/v1/jobs/{id}/result, comprueba los bytes con qpdf --check y
elimina el trabajo finalizado.
Cada respuesta que aparece a continuación es una captura literal de un
despliegue real de Connect de nivel core (nextpdf/server bajo
RoadRunner, enlazado a http://localhost:8080). La única sustitución es
la clave de API, mostrada como la variable de entorno
$NEXTPDF_CONNECT_TOKEN; los identificadores de trabajo, los
identificadores de solicitud, las marcas de tiempo, las cabeceras y los
bytes del cuerpo son exactamente lo que devolvió el servidor. Los valores
de cabecera como Date y X-Request-Id diferirán, por supuesto, en cada
despliegue.
Esta receta procesa un único documento para poder leer cada intercambio
por completo. Para múltiples documentos, concurrencia acotada y bucles de
sondeo guiados por Retry-After, consultar
Generar PDF por lotes con seguimiento de progreso,
que utiliza la misma superficie de trabajos.
Instalación
Sección titulada «Instalación»El lado del servidor es la distribución estándar de Connect:
composer require nextpdf/serverEl lado del cliente de esta receta es curl más qpdf, de modo que puede
portarse a cualquier cliente HTTP. Exportar primero los valores del
despliegue:
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:Resumen conceptual
Sección titulada «Resumen conceptual»La superficie de trabajos asíncronos separa el envío de la recuperación:
se envía una solicitud de renderizado, se recibe un registro de trabajo y
se obtiene el resultado cuando el trabajo alcanza completed. La propia
solicitud de renderizado es un array operations ordenado —los mismos
tipos de operación (set_font, add_text, add_table, add_image,
add_page) que respaldan las llamadas de herramienta de Connect en todos
los transportes— más campos a nivel de documento (page_size,
orientation, title, author).
Dos detalles del contrato dan forma a la transcripción que se va a leer a continuación:
- Envío idempotente. Un envío identificado con
Idempotency-Keydevuelve201 Createdla primera vez y200 OKcon el mismo registro de trabajo al repetirse, de modo que un reintento de red nunca renderiza dos veces. - El envío puede ser ya terminal. La versión actual procesa el trabajo
en línea antes de responder al
POST, por lo que la respuesta del envío puede llevar yastatus: "completed", como ocurre más abajo. El contrato de sondear-hasta-el-estado-terminal es la forma estable de la API: escribir el bucle de sondeo y aceptar un estado terminal en cualquier intento, incluido el primero.
Es posible confirmar lo que expone el despliegue antes de enviar nada:
GET /api/v1/capabilities devuelve el catálogo de operaciones al que
puede acceder el nivel de la clave de API. En el despliegue de nivel core
capturado aquí solo aparecían las operaciones core; el catálogo de
referencia es siempre la propia respuesta del servidor en ejecución, no
esta página.
Superficie de la API
Sección titulada «Superficie de la API»| Intercambio | Método y ruta | Estado capturado |
|---|---|---|
| Enviar el trabajo de renderizado | POST /api/v1/jobs | 201 Created |
| Repetir el mismo envío | POST /api/v1/jobs (misma Idempotency-Key) | 200 OK |
| Sondear el registro del trabajo | GET /api/v1/jobs/{id} | 200 OK |
| Descargar el PDF | GET /api/v1/jobs/{id}/result | 200 OK, application/pdf |
| Eliminar el trabajo finalizado | DELETE /api/v1/jobs/{id} | 204 No Content |
La autenticación es un token bearer en cada solicitud /api/v1/*:
Authorization: Bearer npk_live_{kid}_{secret}. Las respuestas JSON
correctas comparten el envoltorio { "data": ..., "meta": ... }; los
campos sobre los que se actúa están bajo data.
La solicitud de la factura
Sección titulada «La solicitud de la factura»Escribir la solicitud de renderizado en invoice.json. Es una lista de
operaciones sencilla y determinista: una línea de cabecera en negrita, una
línea de emisión y una tabla de líneas de detalle:
{ "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>" } ]}Los campos de la factura aquí son datos de ejemplo. La forma de argumentos
autoritativa para cada operación es la que informa el despliegue: sobre
MCP, tools/list devuelve el esquema de entrada completo para cada tipo
de operación que utiliza esta solicitud.
Transcripción de principio a fin
Sección titulada «Transcripción de principio a fin»1. Enviar el trabajo de renderizado
Sección titulada «1. Enviar el trabajo de renderizado»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.jsonEl servidor responde 201 Created:
HTTP/1.1 201 CreatedCache-Control: no-storeContent-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'Content-Type: application/json; charset=utf-8Referrer-Policy: no-referrerVary: Accept-EncodingX-Content-Type-Options: nosniffX-Frame-Options: DENYX-Powered-By: NextPDF ConnectX-Ratelimit-Remaining: 99X-Request-Id: 019f3fd6-c6ed-727c-958e-2fa4370ba97eDate: Wed, 08 Jul 2026 03:47:48 GMTTransfer-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" }}El trabajo ya es terminal en esta captura —status es "completed" y
result_url está presente— porque la versión actual renderiza en línea
antes de responder. No conviene depender de ello: tratar la respuesta del
envío como el primer resultado de sondeo y ramificar según data.status
como en cualquier otro sondeo.
2. Repetir el envío (ruta idempotente)
Sección titulada «2. Repetir el envío (ruta idempotente)»Repetir exactamente el mismo comando, con la misma Idempotency-Key y el
mismo cuerpo:
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.jsonEl servidor devuelve 200 OK —no 201— con el mismo job_id, y no se
produce un segundo renderizado (comparar meta.duration_ms con la primera
respuesta):
HTTP/1.1 200 OKCache-Control: no-storeContent-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'Content-Type: application/json; charset=utf-8Referrer-Policy: no-referrerVary: Accept-EncodingX-Content-Type-Options: nosniffX-Frame-Options: DENYX-Powered-By: NextPDF ConnectX-Ratelimit-Remaining: 99X-Request-Id: 019f3fd6-c756-7226-bc85-b255d75bd449Date: Wed, 08 Jul 2026 03:47:48 GMTTransfer-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" }}3. Sondear el registro del trabajo
Sección titulada «3. Sondear el registro del trabajo»curl -sS -i "$NEXTPDF_CONNECT_URL/api/v1/jobs/job_9bd0808960f10eb568484acc" \ -H "Authorization: Bearer $NEXTPDF_CONNECT_TOKEN"HTTP/1.1 200 OKCache-Control: no-storeContent-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'Content-Type: application/json; charset=utf-8Referrer-Policy: no-referrerVary: Accept-EncodingX-Content-Type-Options: nosniffX-Frame-Options: DENYX-Powered-By: NextPDF ConnectX-Ratelimit-Remaining: 98X-Request-Id: 019f3fd7-0038-7131-b71e-4c69e637233bDate: Wed, 08 Jul 2026 03:48:02 GMTTransfer-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" }}Este sondeo muestra un registro terminal, por lo que no hay cabecera
Retry-After ni campo poll_url. Mientras un trabajo sigue en pending
o running, el servidor establece Retry-After (un intervalo de 2
segundos) en cada sondeo; conviene respetarlo en lugar de sondear en un
bucle ajustado.
4. Descargar el PDF
Sección titulada «4. Descargar el PDF»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.pdfHTTP/1.1 200 OKCache-Control: no-storeContent-Disposition: attachment; filename="job-job_9bd0808960f10eb568484acc.pdf"Content-Length: 3663Content-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'Content-Type: application/pdfReferrer-Policy: no-referrerVary: Accept-EncodingX-Content-Type-Options: nosniffX-Frame-Options: DENYX-Powered-By: NextPDF ConnectX-Ratelimit-Remaining: 98X-Request-Id: 019f3fd7-0056-711e-acad-9f7ac5f21ba7Date: Wed, 08 Jul 2026 03:48:02 GMTEl cuerpo es el binario del PDF —3663 bytes en esta captura, coincidiendo
con la cabecera Content-Length— y se omite aquí. Se escribe en
invoice-inv-2026-0042.pdf.
5. Verificar localmente los bytes descargados
Sección titulada «5. Verificar localmente los bytes descargados»Un 200 con Content-Type: application/pdf no es, por sí solo, prueba de
que el cuerpo sea un PDF bien formado. Ejecutar una comprobación
estructural con qpdf:
qpdf --check invoice-inv-2026-0042.pdfSalida capturada para el archivo descargado anteriormente:
checking invoice-inv-2026-0042.pdfPDF Version: 2.0File is not encryptedFile is not linearizedNo syntax or stream encoding errors found; the file may still containerrors that qpdf cannot detectLa propia redacción de qpdf marca el límite honesto: se trata de una comprobación de sintaxis y flujos, no de una determinación de conformidad frente a ninguna norma.
6. Eliminar el trabajo finalizado
Sección titulada «6. Eliminar el trabajo finalizado»curl -sS -i -X DELETE "$NEXTPDF_CONNECT_URL/api/v1/jobs/job_9bd0808960f10eb568484acc" \ -H "Authorization: Bearer $NEXTPDF_CONNECT_TOKEN"HTTP/1.1 204 No ContentCache-Control: no-storeContent-Security-Policy: default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'Content-Type: application/json; charset=utf-8Referrer-Policy: no-referrerVary: Accept-EncodingX-Content-Type-Options: nosniffX-Frame-Options: DENYX-Powered-By: NextPDF ConnectX-Ratelimit-Remaining: 97X-Request-Id: 019f3fd7-0084-722f-bf54-3dabead8aeeaDate: Wed, 08 Jul 2026 03:48:02 GMTTras la eliminación, el registro del trabajo y su resultado almacenado
desaparecen; un GET posterior sobre el trabajo devuelve 404.
Casos límite y trampas
Sección titulada «Casos límite y trampas»- Ramificar según
data.status, no solo según el estado HTTP. El envío, la repetición y el sondeo devuelven todos2xxcon un registro de trabajo; el estado del ciclo de vida vive endata.status(pending,running,completed,failed,cancelled). - Una clave repetida con un cuerpo diferente es un
409 Conflict. La repetición idempotente con200solo ocurre cuando el cuerpo coincide con el envío original. No reutilizar nunca una clave para contenido diferente. /resultantes de completarse es un409. Descargar solo después de que un sondeo muestrecompleted. El409es una respuesta normal que inspeccionar, no un fallo de transporte: la misma separación entre transporte y estado que sigue cada receta de Connect (consultar Convenciones de las recetas).- Los trabajos están limitados por propietario. Un trabajo enviado con
una clave de API es invisible para otra clave: un
GETde otro propietario devuelve404, no403. Sondear con la misma credencial con la que se hizo el envío. progresspuede estar ausente. El registro capturado no lleva campoprogressporque el trabajo ya era terminal. Cuando el servidor hace seguimiento del progreso de un trabajo no terminal,data.progresses un entero de 0 a 100; tratar un campo ausente como desconocido, no como cero.- Un trabajo
failedllevadata.error. Registrarlo; no reenviar a ciegas.
Rendimiento
Sección titulada «Rendimiento»Un trabajo de renderizado cuesta un envío, como mucho un puñado de sondeos
y una descarga. Los valores capturados de meta.duration_ms lo cuentan
todo: 63,31 ms para renderizar la factura en el envío, 1,03 ms para la
repetición idempotente que no hizo ningún trabajo y lecturas de estado por
debajo del milisegundo. Sondear con la cadencia Retry-After del servidor
en lugar de un bucle ajustado; la lectura de estado es barata pero no
gratuita, y el limitador de tasa la presupuesta (observar cómo
X-Ratelimit-Remaining desciende en las cabeceras capturadas). Para
lotes, acotar los trabajos en curso en lugar de enviarlo todo de una vez:
la receta de lotes
implementa ese bucle.
Notas de seguridad
Sección titulada «Notas de seguridad»- Mantener el token bearer únicamente en la cabecera
Authorization. Nunca en una cadena de consulta, una línea de registro ni un archivo versionado. La transcripción anterior sustituye una variable de entorno precisamente por esa razón. - Validar los bytes descargados antes de confiar en ellos. El paso 5
forma parte del flujo, no es un extra opcional: comprobar que la
respuesta es un PDF (la cabecera
%PDFcomo mínimo,qpdf --checkpara la estructura) antes de archivarla o reenviarla. - Eliminar los trabajos finalizados que ya no se necesiten. El paso 6 elimina el resultado almacenado del servidor; de lo contrario, un trabajo completado sigue siendo descargable hasta que la recolección de basura de trabajos del servidor lo elimine.
- Usar una clave de mínimo privilegio. Este flujo necesita una clave de renderizado de nivel core y nada más.
Conformidad
Sección titulada «Conformidad»Esta receta no formula ninguna afirmación normativa de estándares.
Ejercita los endpoints REST de trabajos asíncronos de Connect y lee los
campos del registro de trabajo que define el servidor. El paso
qpdf --check confirma únicamente la integridad estructural: «the file
may still contain errors that qpdf cannot detect» es la propia advertencia
de qpdf, citada literalmente más arriba. Determinar la conformidad con un
estándar (PDF/A-4, PDF/UA) es tarea de un validador independiente y de una
superficie diferente; consultar
Ejecutar una comprobación de estándar concreto
para ese límite.
Véase también
Sección titulada «Véase también»- Generar PDF por lotes con seguimiento de progreso — la misma superficie de trabajos gestionada como un lote de concurrencia acotada.
- Generar el primer PDF — el renderizado más pequeño de Connect.
- Gestionar una sesión de documento de agente sobre MCP — el mismo motor, herramienta a herramienta, sobre el transporte stdio de MCP.
- Convenciones de las recetas de Connect — el contrato de transporte, nivel y conformidad que sigue cada receta de Connect.
- Gestión de errores con reconocimiento de excepciones sobre Connect — cómo separar los fallos de transporte de los estados sin éxito.