pdfapi

Node.js и TypeScript

Типы в комплекте, понятные ошибки, повторы при перегрузке. Без зависимостей: работает в Node.js, Bun и Deno; ESM и CommonJS.

ПакетСредыЗависимости
pdfapiNode.js 20.3+, Bun, Denoнет

Установка#

bash
npm install pdfapi

Ключ берётся из переменной окружения PDFAPI_KEY — выпустите его в кабинете:

bash
export PDFAPI_KEY=pdf_live_...

Первый PDF#

ts
import { PdfApi } from 'pdfapi';

const client = new PdfApi(); // ключ из PDFAPI_KEY

const pdf = await client.html('<h1>Счёт №42</h1><p>К оплате: 12 000 ₽</p>', { marginTop: '20mm' });
await pdf.save('invoice.pdf');

console.log(pdf.pages);            // 1 — сколько страниц списано
console.log(pdf.quota?.remaining); // 9857 — сколько осталось в периоде

В CommonJS — то же самое через const { PdfApi } = require('pdfapi').

Методы#

Все методы асинхронные и возвращают Promise.

МетодЧто делает
client.html(html, options?)HTML → PDF
client.url(url, options?)публичная страница по адресу → PDF. Тариф Premium и выше; адрес — полный, с https://
client.markdown(text, options?)Markdown → PDF. Тариф Premium и выше
client.office(file, options?)docx, xlsx, pptx → PDF. Тариф Premium и выше
client.convert(body, options?)POST /v1/convert с телом как есть — для всего остального
client.me(options?)тариф, расход и остаток страниц — GET /v1/me

Параметры страницы#

Все параметры необязательные. Что они значат и как сочетаются, смотрите в разделе Параметры страницы.

ПараметрПримерПо умолчанию
landscapetruefalse
paperWidth, paperHeight'8.27in', '297mm'A4
marginTop, marginBottom, marginLeft, marginRight'20mm''0.39in'
waitDelay'2s', '500ms', до 10 секунд'0s'
ts
const pdf = await client.url('https://example.com/report', { landscape: true, waitDelay: '1s' });

Размер — число и единица pt, px, in, mm, cm или pc без пробела. 20, '20' и '20 mm' вызовут RangeError сразу, без запроса к API.

В тех же options передаются signal и timeout — см. Отмена и таймауты.

Документы Office#

ts
const fromPath = await client.office('report.docx');                         // путь (Node, Bun, Deno)
const fromBytes = await client.office(buffer, { filename: 'report.docx' });  // байты — нужно имя с расширением
const fromForm = await client.office(formData.get('file') as File);          // File из формы — имя берётся из него

file — путь, Uint8Array или Buffer, ArrayBuffer, Blob или File. Файл — до 3,7 МБ: в base64 он вырастает на треть, а тело запроса ограничено 5 МБ. Библиотека проверяет, что файл — настоящий ZIP-архив, поэтому старый .doc или PDF, переименованный в .docx, отклоняется до отправки.

Результат#

PdfResult:

  • content — байты PDF, Uint8Array;
  • pages — сколько страниц списано;
  • quota — limit, used и remaining после этой конвертации;
  • save(path) — записать в файл и вернуть путь;
  • blob() — PDF как Blob с типом application/pdf;
  • size — размер в байтах.

save пишет атомарно: сначала во временный файл рядом, потом переименовывает поверх. Оборвётся запись или кончится место — останется прежний файл, а не обрезанный PDF. console.log(pdf) байты не печатает — только страницы, размер и квоту.

Аккаунт и квота#

ts
const me = await client.me();
console.log(me.plan, me.pagesRemaining, 'из', me.pagesLimit);
console.log('обнулится', me.periodEnd?.toISOString());

Account: userId, plan, periodStart, periodEnd (Date или null), pagesUsed, pagesLimit, pagesRemaining, requestsPerSecond, burst. Запрос страниц не тратит. Как встроить проверку в cron — в руководстве Контроль квоты.

Ошибки#

Все ошибки API — наследники PdfApiError с полями code, status и message по-русски.

ts
import { PdfApiError, QuotaExceededError, RateLimitError, RenderError } from 'pdfapi';

try {
  const pdf = await client.html(html);
} catch (e) {
  if (e instanceof QuotaExceededError) console.log('Страницы кончились:', e.message);
  else if (e instanceof RateLimitError) console.log(`Слишком часто, повторите через ${e.retryAfter} с`);
  else if (e instanceof RenderError) console.log('Документ не отрендерился — проверьте разметку');
  else if (e instanceof PdfApiError) console.log(e.code, e.status, e.message);
  else throw e;
}
ОшибкаКогда
AuthenticationError401: ключа нет, он неверный или отозван
PermissionDeniedError403: почта не подтверждена или формат недоступен на тарифе
QuotaExceededError402: страницы периода кончились или документ больше остатка
InvalidRequestError400: неверный параметр или адрес во внутреннюю сеть (code === 'url_not_allowed')
PayloadTooLargeError413: тело больше 5 МБ; наследник InvalidRequestError
RateLimitError429: слишком часто; retryAfter — через сколько секунд можно снова
RenderError502: документ не отрендерился
RenderTimeoutError504: рендер дольше 30 секунд; наследник RenderError
UnavailableError503: рендер временно недоступен
APIConnectionErrorнет связи с API
APITimeoutErrorAPI не ответил за timeout; наследник APIConnectionError

Ошибки в аргументах — пустой HTML, адрес без https://, файл .pdf вместо .docx — обычные TypeError и RangeError. Они приходят отклонённым Promise, как и ошибки API, но до запроса и без траты лимита частоты. Все коды API — в разделе Ошибки и повторы.

Повторы#

Библиотека сама повторяет запрос, когда это безопасно, — по умолчанию до двух раз (maxRetries):

  • 429 — через Retry-After секунд, но не дольше минуты;
  • 503 или нет соединения (ECONNREFUSED, DNS) — первый повтор примерно через 0,5 секунды, второй — примерно через 1 секунду.
Таймауты не повторяются

После таймаута результат может быть уже сохранён. Повтор с тем же Idempotency-Key и телом запроса защищает от повторного списания на обновлённом сервере. Без этого ключа повтор считается новой конвертацией. Подробнее.

Отмена и таймауты#

ts
const controller = new AbortController();
const pdf = await client.html(html, { signal: controller.signal, timeout: 10_000 });
  • timeout — миллисекунды на одну попытку; по умолчанию — таймаут клиента, 40 000.
  • Отменили через signal — Promise отклоняется вашей причиной из controller.abort(reason), а не ошибкой API.

Настройки клиента#

ts
const client = new PdfApi({
  apiKey: 'pdf_live_...',            // иначе PDFAPI_KEY
  baseUrl: 'https://pdfapi.ru/api',  // иначе PDFAPI_BASE_URL
  timeout: 40_000,                   // мс на один запрос
  maxRetries: 2,
  fetch: myFetch,                    // свой fetch — например, через прокси
});
  • Один клиент на приложение. Создайте его один раз и переиспользуйте — закрывать ничего не нужно.
  • Неверные настройки — timeout не больше нуля, maxRetries меньше нуля или дробное — RangeError сразу.
  • Ключ не утекает — он в приватном поле: его нет ни в console.log(client), ни в JSON.stringify(client), ни в текстах ошибок.
Только на сервере

Ключ в коде страницы увидит любой пользователь, поэтому в браузере new PdfApi() выбрасывает ошибку. Вызывайте API со своего сервера, а браузеру отдавайте готовый PDF.

PDF в веб-приложении#

PDF уже в памяти — отдавайте байты, на диск сохранять незачем:

ts
// app/invoice/route.ts — так же в Hono и любом обработчике на Request/Response
import { PdfApi } from 'pdfapi';

const client = new PdfApi();

export async function GET() {
  const pdf = await client.html('<h1>Счёт №42</h1>');
  return new Response(pdf.blob(), {
    headers: { 'Content-Disposition': 'inline; filename="invoice.pdf"' },
  });
}

Данные пользователя в HTML — только с экранированием, иначе это HTML-инъекция в документ — см. Безопасность и данные.

Deno#

ts
import { PdfApi } from 'npm:pdfapi';
bash
deno run --allow-net=pdfapi.ru --allow-env=PDFAPI_KEY,PDFAPI_BASE_URL app.ts

--allow-env не нужен, если ключ и адрес переданы в new PdfApi({...}) явно. Для save() и office('путь') добавьте --allow-write и --allow-read.

Примеры#

В папке examples/ библиотеки; Node.js 22.18+ запускает их без компиляции: npm run build && PDFAPI_KEY=pdf_live_... node examples/<файл>.

ФайлЧто показывает
invoice.tsсчёт из HTML-шаблона с экранированием данных
from-url.tsстраница по адресу, альбомная, с ожиданием скриптов
office.tsdocx, xlsx или pptx в PDF рядом с исходным файлом
batch.tsпачка документов параллельно, в пределах лимита частоты
quota.tsостаток страниц для cron: код выхода 2 при нехватке
server.tsотдача PDF из node:http, ошибки API — в HTTP-ответы