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

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