Перейти к содержимому
getnextpdf.com

стабильность: Бета

Сквозная отрисовка счёта через 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-клиент. Сначала экспортируйте значения вашего развёртывания:

/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 — те же типы операций (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; каталог, имеющий силу, — это всегда собственный ответ работающего сервера, а не эта страница.

ОбменМетод и путьЗахваченный статус
Отправить задачу отрисовкиPOST /api/v1/jobs201 Created
Повторить ту же отправкуPOST /api/v1/jobs (тот же Idempotency-Key)200 OK
Опросить запись задачиGET /api/v1/jobs/{id}200 OK
Загрузить PDFGET /api/v1/jobs/{id}/result200 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 возвращает полную входную схему для каждого типа операции, который использует этот запрос.

Окно терминала
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, то же тело:

Окно терминала
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 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"
}
}
Окно терминала
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 секунды) на каждом опросе — соблюдайте его вместо опроса в плотном цикле.

Окно терминала
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 — 3663 байта в этом захвате, что соответствует заголовку Content-Length — и здесь опущено. Оно записывается в invoice-inv-2026-0042.pdf.

200 с Content-Type: application/pdf сам по себе не является доказательством того, что тело — это корректно сформированный PDF. Выполните структурную проверку с помощью qpdf:

Окно терминала
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 — это честная граница: это проверка синтаксиса и потоков, а не определение соответствия какому-либо стандарту.

Окно терминала
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

После удаления запись задачи и её сохранённый результат исчезают; последующий 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) — это работа независимого валидатора и другой интерфейс — об этой границе см. Проверка по именованному стандарту.