стабильность: Бета
Сквозная отрисовка счёта через REST
Проведите один счёт от JSON до локально проверенного PDF через
интерфейс Representational State Transfer (REST) NextPDF Connect, по
одному сетевому обмену за раз. Этот рецепт отправляет задачу отрисовки в
POST /api/v1/jobs, повторяет отправку с тем же Idempotency-Key, чтобы
показать безопасный при дублировании путь, опрашивает
GET /api/v1/jobs/{id}, загружает PDF из
GET /api/v1/jobs/{id}/result, проверяет байты через qpdf --check и
удаляет завершённую задачу.
Каждый ответ ниже — это дословный захват с реального развёртывания
Connect уровня core (nextpdf/server под RoadRunner, привязанного к
http://localhost:8080). Единственная замена — это API-ключ, показанный
как переменная окружения $NEXTPDF_CONNECT_TOKEN; идентификаторы задач,
идентификаторы запросов, метки времени, заголовки и байты тела — это в
точности то, что вернул сервер. Значения заголовков, такие как Date и
X-Request-Id, разумеется, будут отличаться на вашем развёртывании.
Этот рецепт обрабатывает один документ, чтобы вы могли прочитать каждый
обмен целиком. Для множества документов, ограниченного параллелизма и
циклов опроса, управляемых Retry-After, см.
Пакетная генерация PDF с отслеживанием прогресса,
где используется тот же интерфейс задач.
Установка
Заголовок раздела «Установка»Серверная сторона — это стандартная поставка Connect:
composer require nextpdf/serverКлиентская сторона этого рецепта — это curl плюс qpdf, так что вы
можете перенести его на любой HTTP-клиент. Сначала экспортируйте значения
вашего развёртывания:
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 — те же типы операций
(set_font, add_text, add_table, add_image, add_page), которые
лежат в основе вызовов инструментов Connect на любом транспорте — плюс
поля уровня документа (page_size, orientation, title, author).
Две детали контракта формируют стенограмму, которую вы сейчас прочитаете:
- Идемпотентная отправка. Отправка с ключом
Idempotency-Keyвозвращает201 Createdв первый раз и200 OKс той же самой записью задачи при повторе, так что сетевой повтор никогда не отрисует документ дважды. - Отправка может уже быть терминальной. Текущий релиз обрабатывает
задачу встроенно перед ответом на
POST, так что ответ на отправку может уже нестиstatus: "completed"— как это происходит ниже. Контракт «опрос до терминального состояния» — это стабильная форма API: напишите цикл опроса и принимайте терминальное состояние на любой попытке, включая первую.
Вы можете убедиться, что именно предоставляет ваше развёртывание, ещё до
того, как что-либо отправите: GET /api/v1/capabilities возвращает
каталог операций, доступный уровню вашего API-ключа. На развёртывании
уровня core, захваченном здесь, он перечислил только операции core;
каталог, имеющий силу, — это всегда собственный ответ работающего
сервера, а не эта страница.
Поверхность API
Заголовок раздела «Поверхность API»| Обмен | Метод и путь | Захваченный статус |
|---|---|---|
| Отправить задачу отрисовки | POST /api/v1/jobs | 201 Created |
| Повторить ту же отправку | POST /api/v1/jobs (тот же Idempotency-Key) | 200 OK |
| Опросить запись задачи | GET /api/v1/jobs/{id} | 200 OK |
| Загрузить PDF | GET /api/v1/jobs/{id}/result | 200 OK, application/pdf |
| Удалить завершённую задачу | DELETE /api/v1/jobs/{id} | 204 No Content |
Аутентификация — это bearer-токен на каждом запросе /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 возвращает полную входную схему для каждого типа операции,
который использует этот запрос.
Сквозная стенограмма
Заголовок раздела «Сквозная стенограмма»1. Отправка задачи отрисовки
Заголовок раздела «1. Отправка задачи отрисовки»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 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" }}Задача в этом захвате уже терминальна — status равен "completed", а
result_url присутствует — потому что текущий релиз отрисовывает
встроенно перед ответом. Не полагайтесь на это: рассматривайте ответ на
отправку как первый результат опроса и ветвитесь по data.status, как на
любом другом опросе.
2. Повтор отправки (идемпотентный путь)
Заголовок раздела «2. Повтор отправки (идемпотентный путь)»Повторите ту же самую команду — тот же Idempotency-Key, то же тело:
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Сервер возвращает 200 OK — а не 201 — с тем же job_id, и второй
отрисовки не происходит (сравните meta.duration_ms с первым ответом):
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. Опрос записи задачи
Заголовок раздела «3. Опрос записи задачи»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" }}Этот опрос показывает терминальную запись, поэтому нет ни заголовка
Retry-After, ни поля poll_url. Пока задача всё ещё находится в
состоянии pending или running, сервер устанавливает Retry-After
(интервал в 2 секунды) на каждом опросе — соблюдайте его вместо опроса в
плотном цикле.
4. Загрузка PDF
Заголовок раздела «4. Загрузка 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 GMTТело — это двоичный PDF — 3663 байта в этом захвате, что соответствует
заголовку Content-Length — и здесь опущено. Оно записывается в
invoice-inv-2026-0042.pdf.
5. Локальная проверка загруженных байтов
Заголовок раздела «5. Локальная проверка загруженных байтов»200 с Content-Type: application/pdf сам по себе не является
доказательством того, что тело — это корректно сформированный PDF.
Выполните структурную проверку с помощью qpdf:
qpdf --check invoice-inv-2026-0042.pdfЗахваченный вывод для файла, загруженного выше:
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 detectСобственная формулировка qpdf — это честная граница: это проверка синтаксиса и потоков, а не определение соответствия какому-либо стандарту.
6. Удаление завершённой задачи
Заголовок раздела «6. Удаление завершённой задачи»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 GMTПосле удаления запись задачи и её сохранённый результат исчезают;
последующий GET на этой задаче возвращает 404.
Граничные случаи и подводные камни
Заголовок раздела «Граничные случаи и подводные камни»- Ветвитесь по
data.status, а не только по HTTP-статусу. Отправка, повтор и опрос — все возвращают2xxс записью задачи; состояние жизненного цикла находится вdata.status(pending,running,completed,failed,cancelled). - Повторённый ключ с другим телом — это
409 Conflict. Идемпотентный повтор с200происходит только тогда, когда тело совпадает с исходной отправкой. Никогда не используйте ключ повторно для другого содержимого. /resultдо завершения — это409. Загружайте только после того, как опрос покажетcompleted.409— это нормальный ответ для рассмотрения, а не сбой транспорта — то же самое разделение транспорта и статуса, которому следует каждый рецепт Connect (см. Соглашения по рецептам).- Задачи привязаны к владельцу. Задача, отправленная под одним
API-ключом, невидима для другого ключа:
GETот чужого владельца возвращает404, а не403. Опрашивайте с теми учётными данными, с которыми вы отправляли. progressможет отсутствовать. Захваченная запись не несёт поляprogress, потому что задача уже была терминальной. Когда сервер отслеживает прогресс для нетерминальной задачи,data.progress— это целое число от 0 до 100; трактуйте отсутствующее поле как неизвестное, а не как ноль.- Задача с состоянием
failedнесётdata.error. Зафиксируйте её; не переотправляйте вслепую.
Производительность
Заголовок раздела «Производительность»Одна задача отрисовки стоит одной отправки, самое большее — нескольких
опросов и одной загрузки. Захваченные значения meta.duration_ms
говорят сами за себя: 63,31 мс на отрисовку счёта при отправке, 1,03 мс
на идемпотентный повтор, который не выполнил никакой работы, и
субмиллисекундные чтения статуса. Опрашивайте в ритме Retry-After
сервера, а не в плотном цикле; чтение статуса дёшево, но не бесплатно, и
ограничитель частоты учитывает его в бюджете (смотрите, как X-Ratelimit-Remaining
убывает в захваченных заголовках). Для пакетов ограничивайте число задач
в работе вместо отправки всего сразу — этот цикл реализован в
рецепте пакетной обработки.
Замечания по безопасности
Заголовок раздела «Замечания по безопасности»- Держите bearer-токен только в заголовке
Authorization. Никогда в строке запроса, строке журнала или зафиксированном в репозитории файле. Стенограмма выше подставляет переменную окружения именно по этой причине. - Проверяйте загруженные байты, прежде чем доверять им. Шаг 5 — это
часть потока, а не необязательное дополнение: убедитесь, что ответ —
это PDF (как минимум заголовок
%PDF,qpdf --checkдля структуры), прежде чем архивировать или пересылать его. - Удаляйте завершённые задачи, которые вам больше не нужны. Шаг 6 удаляет сохранённый результат с сервера; в противном случае завершённая задача остаётся доступной для загрузки, пока сборка мусора задач сервера не удалит её.
- Используйте ключ с минимальными привилегиями. Этому потоку нужен ключ отрисовки уровня core и ничего более.
Соответствие
Заголовок раздела «Соответствие»Этот рецепт не делает никаких нормативных заявлений о соответствии
стандартам. Он использует REST-конечные точки асинхронных задач Connect и
читает поля записи задачи, которые определяет сервер. Шаг qpdf --check
подтверждает только структурную целостность — «the file may still contain
errors that qpdf cannot detect» — это собственная оговорка qpdf,
процитированная дословно выше. Определение соответствия стандарту
(PDF/A-4, PDF/UA) — это работа независимого валидатора и другой интерфейс
— об этой границе см.
Проверка по именованному стандарту.
См. также
Заголовок раздела «См. также»- Пакетная генерация PDF с отслеживанием прогресса — тот же интерфейс задач, управляемый как пакет с ограниченным параллелизмом.
- Сгенерируйте свой первый PDF — наименьшая отрисовка Connect.
- Управление сессией документа агента через MCP — тот же движок, инструмент за инструментом, через транспорт MCP stdio.
- Соглашения по рецептам Connect — контракт транспорта, уровня и соответствия, которому следует каждый рецепт Connect.
- Обработка ошибок с учётом исключений через Connect — как отделять сбои транспорта от неуспешных статусов.