How-to2026-09-289 мин чтения

TypeScript SDK для PDF API: пять минут до первого вызова и то, что он оставляет вам

С @pdf123/sdk из TypeScript можно объединять файлы, ставить водяные знаки, читать сведения о файле и собирать несколько инструментов в один запрос. SDK ловит опечатку в имени инструмента или параметра ещё до загрузки; отказы сервера приходят с причиной и подсказкой, а повторы, отмена и частичные сбои в пакете остаются на вашей стороне.

PDF123 · Updated 2026-09-28

Когда вы вызываете PDF API из Node, набор для разработки (SDK) @pdf123/sdk ловит опечатку в имени инструмента или параметра до того, как загружен хоть один файл. Он не повторяет запросы, не отменяет их за вас и не отдаёт результат потоком. new Pdf123Client() по умолчанию анонимно обращается к https://pdf123.xyz; ваши файлы загружаются туда и там же обрабатываются, потому что локального движка нет.

Схема: один вызов проходит три барьера по очереди — проверка tsc при компиляции, проверка во время выполнения до отправки запроса и ответ 400, который сервер возвращает после загрузки; повторы и отмена остаются на стороне вызывающего, а входные данные суммарным размером больше 95 MiB загружаются частями автоматически

Для объединения хватит двух PDF в папке

Нужен Node 20.3 или новее, либо Bun. Пакет — ES-модуль: положите код в файл .mjs или сначала выполните npm pkg set type=module. Положите a.pdf и b.pdf в текущий каталог.

npm install @pdf123/sdk
// merge.mjs
import { Pdf123Client } from "@pdf123/sdk";
import { readFileInput, saveResult } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const merged = await client.run("merge", {
  input: [await readFileInput("a.pdf"), await readFileInput("b.pdf")],
});
console.log(await saveResult(merged, { output: "merged.pdf" }));

После node merge.mjs терминал выводит переданный вами путь merged.pdf, и файл лежит в текущем каталоге. readFileInput и saveResult находятся в @pdf123/sdk/node. Корневая точка входа зависит только от fetch и принимает входные данные в виде простого { name, data }, поэтому тот же клиент подойдёт и среде без файловой системы. Результат — это { data, contentType, filename, json }; data — целый Uint8Array, а Buffer.from(result.data) даёт Buffer.

Объединить PDF — лишь один из инструментов.

Файлы приходят в data, отчёты — в json

Любой инструмент вызывается как client.run(toolName, { input, params }). Код ниже продолжает merge.mjs из предыдущего раздела, client и saveResult уже в области видимости. Откуда читать результат, зависит от поля returns из getTool, которое равно "file" или "json": для первого берите data, для второго — json.

import { getTool } from "@pdf123/sdk";
import { readFileInput, saveResult } from "@pdf123/sdk/node";

// client взят из merge.mjs предыдущего раздела
const marked = await client.run("watermark", {
  input: await readFileInput("a.pdf"),
  params: { watermarkText: "DRAFT", fontSize: 40 },
});
console.log(await saveResult(marked, { output: "marked.pdf" }));

const info = await client.run("get-info", { input: await readFileInput("a.pdf") });
console.log(info.json.FileSize, info.json.Encrypted);
console.log(getTool("get-info")?.returns); // "json"

Первый блок записывает файл с водяным знаком в marked.pdf; во втором info.json — это отчёт. Названия полей, значения по умолчанию и допустимые значения запоминать не нужно: каталог — это getTool("watermark")?.fields, и тот же самый читает pdfx describe watermark в командной строке. В TOOLS лежат все 95 инструментов. Полный справочник — в руководстве по SDK на странице для разработчиков.

Чтобы объединить, затем поставить водяной знак и затем сжать, можно взять pipeline или три вызова run подряд; что выбрать, зависит от того, нужны ли вам промежуточные файлы. Здесь тоже продолжается client из кода выше. pipeline сводит три шага в один запрос, промежуточные результаты остаются на сервере, а вызывающий получает только последний шаг, который код ниже записывает в out.pdf. Если нужен файл после каждого шага, вызывайте их по отдельности.

const result = await client.pipeline(
  [{ tool: "merge" }, { tool: "watermark", params: { watermarkText: "DRAFT" } }, { tool: "compress" }],
  [await readFileInput("a.pdf"), await readFileInput("b.pdf")],
);
console.log(await saveResult(result, { output: "out.pdf" }));

В конвейере не больше 8 шагов. На 9-й шаг приходит HTTP 400: at most 8 pipeline steps allowed.

Анонимно, с ключом или на ваш собственный сервер

Если вы задали PDFX_API_BASE, а запрос всё равно уходит на https://pdf123.xyz, то причина в том, что new Pdf123Client() не читает переменные окружения: он смотрит только на параметры конструктора.

Для анонимных вызовов используйте конструктор как есть. С ключом пишите new Pdf123Client({ apiKey: process.env.PDFX_API_KEY }); заголовок запроса — X-API-KEY.

Чтобы указать на свой сервер, возьмите clientFromEnv() из @pdf123/sdk/node. Он читает PDFX_API_BASE и PDFX_API_KEY. Можно также написать прямо new Pdf123Client({ baseUrl: "http://localhost:8080" }). Если адрес недоступен, вызов завершается ошибкой; автономного режима нет. Что вы получаете и чего это стоит, когда файлы остаются в вашей сети, разобрано в Что на самом деле даёт самостоятельный хостинг (и чего он стоит).

Неверный параметр ловится до загрузки

Без этого файл сначала загрузился бы, и опечатка в имени поля обнаружилась бы только как ответ 400 от сервера. Типы параметров каждого инструмента генерируются из каталога, поэтому три частые ошибки падают уже на этапе tsc:

Что вы пишете Ошибка компилятора (фрагмент)
client.run("rotat", { input }) Argument of type '"rotat"' is not assignable to parameter of type 'ToolId'
params: { watermarkTxt: "DRAFT" } Object literal may only specify known properties, but 'watermarkTxt' does not exist, а в конце Did you mean to write 'watermarkText'?
params: { angle: 45 } (rotate принимает только 90, 180, 270) Type '45' is not assignable to type …, далее допустимые значения

Верные вызовы вроде { angle: 90 } или { watermarkText: "DRAFT", fontSize: 30 } компилируются. Числовые поля принимают и числа, и строки, поэтому fontSize: 30 и fontSize: "30" равнозначны. Проект без TypeScript получает те же ошибки во время выполнения, и запрос не отправляется: в третьем случае будет Field "angle" of "rotate" must be one of: 90, 180, 270, а опечатка в имени инструмента даёт unknown_tool, и похожие имена перечислены в сообщении.

Если сервер отказал, ветвитесь по reason

Когда сервер отклоняет запрос, SDK выбрасывает Pdf123Error с полями status, code, reason и problem.hint. У одного code может быть несколько значений reason, поэтому сначала проверяйте reason, а потом откатывайтесь к code. Три частых сбоя, запущенных локально на одном и том же входе, выглядят так:

Вход status code reason hint (оригинал на английском)
Зашифрованный PDF, пароль не указан 400 bad_request password_required Provide the document password, or unlock the PDF first
Неверный пароль 400 bad_request wrong_password Check the password and try again
Не PDF или повреждённый файл 400 invalid_document invalid_pdf Upload a valid, undamaged PDF; you can try repairing it first
import { Pdf123Client, Pdf123Error } from "@pdf123/sdk";
import { readFileInput } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const input = await readFileInput("locked.pdf");
try {
  await client.run("compress", { input });
} catch (error) {
  if (!(error instanceof Pdf123Error)) throw error;
  console.error(error.status, error.code, error.reason, error.problem?.hint);
  if (error.reason === "password_required") {
    await client.run("compress", { input, password: "secret" });
  }
}

Если пароль известен, передайте password для одного файла; снятие защиты и нужный инструмент выполняются в одном запросе. С повреждённым файлом сначала попробуйте Исправить PDF.

«Нет результата» нужно отличать от ошибки. Когда PDF в CSV не находит в PDF ни одной таблицы, сервер возвращает 204, а SDK выбрасывает code: "no_content"; причина разобрана в Пустой экспорт таблиц (204): скорее всего, в вашем PDF нет колонок. Инструменты условной фильтрации считают ложное условие штатным исходом: они не выбрасывают ошибку и возвращают matched: false с пустым data.

Повторы, память и отмена — на стороне вызывающего

Сетевые ошибки и ответы 5xx выбрасываются как есть; SDK сам никогда не повторяет запрос. Для запроса, который меняет состояние, передайте собственный idempotencyKey: повторная отправка с тем же ключом вернёт первый результат, а не обработает файл заново; механизм и ограничения описаны в Idempotency-Key: безопасные повторные попытки для PDF-задач. Результаты читаются в память целиком, потоковой выдачи нет. Когда велики и вход, и выход, считайте память, которую они занимают одновременно.

Отмена и тайм-аут — это два разных значения code, и один вызов сообщает только то, что наступило раньше. В примере ниже они проверяются в двух отдельных вызовах, чтобы обе ветки действительно могли выполниться:

import { Pdf123Client, Pdf123Error } from "@pdf123/sdk";
import { readFileInput } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const input = await readFileInput("a.pdf");

const controller = new AbortController();
setTimeout(() => controller.abort(), 10_000);
try {
  await client.run("compress", { input, signal: controller.signal });
} catch (error) {
  if (error instanceof Pdf123Error && error.code === "cancelled") { /* вы отменили вызов */ }
}

try {
  await client.run("compress", { input, timeoutMs: 60_000 });
} catch (error) {
  if (error instanceof Pdf123Error && error.code === "timeout") { /* превышен timeoutMs; на этот раз без signal */ }
}

Как только срабатывает сигнал прерывания (AbortSignal), запрос отклоняется с code: "cancelled"; превышение тайм-аута даёт code: "timeout". У каждого запроса тайм-аут по умолчанию 5 минут; его можно изменить при создании клиента или переопределить для отдельного вызова через timeoutMs. При загрузке частями каждый запрос несёт этот тайм-аут сам по себе. Отмена лишь закрывает это соединение; гарантии, что сервер прекратит обработку, нет.

Один файл упал, остальные продолжают, а колбэки приходят в порядке завершения

Для пакета входных файлов в инструменте для одного файла используйте runBatch. Если один файл завершился ошибкой, остальные продолжают работу. onResult вызывается в момент завершения каждого файла, то есть в порядке завершения, а index — позиция файла во входном массиве; возвращаемый массив всегда идёт в порядке входа. Записывайте байты на диск внутри этого колбэка: retainData: false очищает data, когда колбэк возвращается, поэтому результат, который вы здесь не передали в saveResult, пропадёт. Если запуск прервали, уже записанные файлы остаются.

Допустим, в папке три нормальных PDF — a.pdf, b.pdf и зашифрованный locked.pdf (пароль secret), — а также сломанный файл broken.pdf, в котором лишь несколько набранных символов:

import { Pdf123Client } from "@pdf123/sdk";
import { fileSource, saveResult } from "@pdf123/sdk/node";

const client = new Pdf123Client();
const results = await client.runBatch("compress", [
  fileSource("a.pdf"), fileSource("broken.pdf"), fileSource("b.pdf"), fileSource("locked.pdf"),
], {
  concurrency: 2,
  passwordFor: (file) => (file.name === "locked.pdf" ? "secret" : undefined),
  onResult: async (entry, index) => {
    console.log(index, entry.input.name, entry.ok ? "ok" : entry.error.reason);
    if (entry.ok) await saveResult(entry.result, { output: "out/" });
  },
  retainData: false,
});

По умолчанию одновременно обрабатываются два файла; меняется это через concurrency. fileSource(path) читает файл с диска только тогда, когда подходит его очередь, поэтому большой пакет больших файлов никогда не лежит в памяти целиком. passwordFor позволяет за один прогон обработать пакет, где перемешаны защищённые и обычные файлы. При локальном запуске на четырёх файлах выше колбэк получил:

1 broken.pdf invalid_pdf
0 a.pdf ok
2 b.pdf ok
3 locked.pdf ok

Порядок завершения может быть другим при каждом запуске; это лишь то, как выглядел один из прогонов. retainData: false делает data в возвращённом массиве пустым; успешные файлы уже записаны в out/ вызовом saveResult выше. Если передать idempotencyKey в runBatch, то на каждый файл фактически отправляется ключ <key>:<index>.

Загрузка частями начинается только при общем размере больше 95 MiB

Тело прямого запроса ограничено 100 MiB, и всё, что больше, отклоняется; подробности — в Когда большой PDF не загружается: лимит тела запроса 100 MiB и ошибка, уводящая не туда. Когда все файлы одного запроса вместе весят больше 95 MiB, SDK переключается на загрузку частями, а ваш вызов client.run не меняется. Максимум для одной загрузки на сервере по умолчанию — 500 MiB; при самостоятельном размещении его меняют через PDFX_UPLOAD_MAX_BYTES.

Эту же операцию, записанную через curl, MCP и командную строку, можно посмотреть в Одна операция, четыре клиента: браузер, curl, MCP, pdfx: там четыре вызова стоят рядом, а здесь подробно разобран только SDK. Страница пакета в npm — @pdf123/sdk.

Open tool
Process in the browser — no watermark, files removed after the job.
Open tool
Read next
Номера страниц в PDF: положение, начальный номер и ловушка повёрнутых страниц
Как пронумеровать страницы готового PDF онлайн: выберите угол, задайте начальный номер, например 101, и избегайте номеров, лежащих боком на страницах, которые хранятся как повёрнутые.
Конвертация PDF в JPG или PNG: что меняет DPI и как экспортировать одну страницу
«PDF в изображения» отрисовывает каждую страницу в ZIP, по умолчанию в PNG, если не выбрать JPEG. В форме браузера есть только формат и DPI; поля API для выбора страниц и WebP игнорируются. Измеренные размеры файлов при 150 и 300 DPI и способ экспортировать одну страницу.
Фото с телефона в PDF: почему файл получается огромным, а страница повёрнута набок
«Изображения в PDF» помещает каждое фото на отдельную страницу в полном размере в пикселях: три JPEG по 3,2 МБ с телефона превратились в PDF на 41,5 МБ, а портретное фото попало на альбомную страницу. Уменьшите фото перед конвертацией, а повёрнутую страницу поверните потом.