How do I install the SDK?
Install the package with your package manager. Node 20.3 or newer, Bun or a bundler is required. TypeScript types are included and @types/node is not needed.
npm install @pdf123/sdkbun add @pdf123/sdk and pnpm add @pdf123/sdk work the same way.
How do I merge two PDFs?
Create a client, run the merge tool on two files and save the result. With no options the client talks to the public API anonymously.
import { Pdf123Client } from "@pdf123/sdk";
import { readFileInput, saveResult } from "@pdf123/sdk/node";
const client = new Pdf123Client();
const result = await client.run("merge", {
input: [await readFileInput("a.pdf"), await readFileInput("b.pdf")],
});
const path = await saveResult(result, { output: "merged.pdf" });
console.log(path);The result holds the bytes in data, the contentType, the server's filename, and json for tools that return a report. The same operation is available as Merge PDF on the website.
Which entry point should I import?
| Entry point | Needs | Provides |
|---|---|---|
@pdf123/sdk | Only fetch | Pdf123Client, TOOLS, getTool, Pdf123Error and the catalog helpers |
@pdf123/sdk/node | Node or Bun | readFileInput, fileSource, saveResult, checkOutput and clientFromEnv |
Import from the root entry in browsers and edge runtimes, and add the node entry only where you read or write files.
Which methods does the client have?
| Method | What it does |
|---|---|
run(tool, { input, params }) | Runs one tool and resolves with a result |
runBatch(tool, inputs, options) | Runs a single-file tool on many inputs and resolves with one entry per input |
pipeline(steps, input, options) | Runs several tools in one request |
call(target, files, fields) | Sends a raw request to a tool id, an operation id or an /api/... path |
upload(file) | Uploads one file through the chunked upload API and returns its id |
Catalog helpers are plain functions: TOOLS lists all tools, getTool(id) returns the fields, defaults and accepted file types of one tool, and toolGroup, toolSummary, matchesQuery and suggestTools help build tool pickers. The command line prints the same catalog with pdfx list and pdfx describe.
How do I pass options to a tool?
params is typed per tool. It accepts only that tool's fields, and choice fields accept only their allowed values. Number fields are checked against their minimum and maximum, and a file is checked against the accepted types, before anything is uploaded. Defaults that the server needs are filled in.
const marked = await client.run("watermark", {
input: await readFileInput("a.pdf"),
params: { watermarkText: "DRAFT", fontSize: 40, rotation: -45 },
});
await saveResult(marked, { output: "draft.pdf" });An empty string means "not set", so the default applies. See Watermark PDF for what each option does.
How do I process many files?
runBatch runs a single-file tool, such as Compress, Rotate or Protect, on each input with the same options. It runs two files at a time by default. One bad file never stops the others.
import { fileSource, saveResult } from "@pdf123/sdk/node";
const controller = new AbortController();
const entries = await client.runBatch("compress", [fileSource("a.pdf"), fileSource("b.pdf")], {
concurrency: 2,
signal: controller.signal,
onResult: async (entry, index) => {
if (entry.ok) await saveResult(entry.result, { output: "compressed/" });
},
});
console.log(entries.map((entry) => entry.ok));fileSource(path)is a lazy source that is read only when a worker takes it, so many large files never sit in memory together.onResultreceives each result as it completes. Save it there, and a cancel at file 50 of 100 keeps the first 49. WithretainData: falsethe returned array does not keep the bytes.- Any call accepts
timeoutMsand asignalto cancel it. idempotencyKeybecomes<key>:<index>for each file.
Filter tools (ids starting with filter-) resolve with matched: false instead of throwing when their condition does not hold. In a batch such a file still counts as ok.
How do I chain tools in one request?
pipeline sends a list of steps and one or more inputs. The server feeds the output of each step into the next.
const piped = await client.pipeline(
[{ tool: "merge" }, { tool: "watermark", params: { watermarkText: "DRAFT" } }, { tool: "compress" }],
[await readFileInput("a.pdf"), await readFileInput("b.pdf")],
);
await saveResult(piped, { output: "out.pdf" });Pipelines accept the same password options as run.
How do I work with encrypted PDFs?
Pass password to run to open an encrypted input first. The unlock and the tool run in one request. For tools that take several files, such as Merge, pass passwords with one entry per input, using an empty string for an open file. Each file is unlocked separately.
await client.run("compress", { input: await readFileInput("locked.pdf"), password: "secret" });
await client.run("merge", {
input: [await readFileInput("locked.pdf"), await readFileInput("open.pdf")],
passwords: ["secret", ""],
});For a batch, passwordFor(file, index) returns the password for each input. To remove protection permanently, use the Unlock tool.
How do errors work?
Failures throw Pdf123Error. It has status (undefined when there was no response), code, reason for the fine-grained cause such as password_required, and problem with the server's problem details, which include hint when there is one. Validation errors are thrown before anything is uploaded.
import { Pdf123Error } from "@pdf123/sdk";
try {
await client.run("compress", { input: await readFileInput("locked.pdf") });
} catch (error) {
if (error instanceof Pdf123Error) {
console.error(error.status, error.code, error.reason, error.problem?.hint);
} else {
throw error;
}
}Server codes are listed on Error codes. The client adds these codes of its own:
| Code | Meaning |
|---|---|
network_error | The request got no response |
timeout | A request exceeded timeoutMs |
cancelled | Your signal aborted the call |
input_unreadable | A local path could not be read |
output_unwritable | The result could not be saved at the given path |
output_mismatch | saveResult refused to write a ZIP to a .pdf name |
unsupported_file_type | The file type is not accepted by the tool |
unknown_tool | The tool id does not exist; the message suggests close ids |
invalid_target | call refused an /api/... path with .., . or empty segments |
no_content | The tool had nothing to return (HTTP 204) |
saveResult never overwrites a file inside a directory. Call checkOutput(output, { several }) first to find out before uploading whether a path can be written.
Which TypeScript and module settings work?
The types resolve under the moduleResolution settings nodenext, node16, bundler and the older node10, including the @pdf123/sdk/node subpath. The package is an ES module, so import works everywhere. require("@pdf123/sdk") from CommonJS works on Node 22.12 or newer and is not available on Node 20.
How do I configure the client?
| Option | Environment variable | Default |
|---|---|---|
baseUrl | PDFX_API_BASE | https://pdf123.xyz |
apiKey | PDFX_API_KEY | none, anonymous |
timeoutMs | none | 300000 |
new Pdf123Client() never reads the environment. clientFromEnv() from @pdf123/sdk/node reads the two variables, and options you pass win. The key is sent as the X-API-KEY header; see Authentication.
To use your own server, set baseUrl to its address, for example http://localhost:8080. See Self-host.
Can I use the SDK in a browser?
Yes. The root entry @pdf123/sdk only needs fetch and has no Node dependencies. Build FileInput objects from a File or a Uint8Array yourself, because the file helpers live in the node entry. Do not ship an API key in browser code that other people can read.
Related pages
- Developers overview with the REST API and authentication
- pdfx command line, built on this SDK
- MCP servers for AI agents
- Swagger UI and the OpenAPI document
- Tool pages: Merge, Split, Compress, OCR, Protect
FAQ
Does the SDK need an API key?
No. A client created without options calls the public API anonymously. Pass apiKey to send the X-API-KEY header.
Which Node version does the SDK need?
Node 20.3 or newer, or Bun. The root entry only needs fetch, so it also runs in browsers and bundlers.
How do I get a tool that is newer than the SDK?
Use client.call. It takes a tool id, an operation id such as general/merge-pdfs or an /api/... path, plus files and fields. Nothing is validated or defaulted locally.
How large a file can I upload?
Inputs over 95 MB in total go through the chunked upload API automatically. A single direct request body is limited to 100 MiB.
Why did my call throw for a PDF with no tables?
Tools such as pdf-to-csv and pdf-to-xlsx answer with no content when the PDF has no detectable table. The SDK throws Pdf123Error with the code no_content instead of returning an empty file. Check that code if an empty result is acceptable.