offlinepdf-sdk

npm install offlinepdf-sdk
import { mergePdfs } from "offlinepdf-sdk";
import fs from "node:fs";

const a = fs.readFileSync("a.pdf");
const b = fs.readFileSync("b.pdf");
const merged = await mergePdfs([a, b]);
fs.writeFileSync("merged.pdf", merged);

That's the whole shape of the API: every function takes raw bytes (Uint8Array) in and returns raw bytes out. No file system access, no network calls, no DOM.

Why this exists

offlinepdf-sdk is extracted from OfflinePDF's own 42 browser-based PDF tools — the same logic that runs inside the website's Web Workers, pulled out of that wrapper and given a plain async API. Just npm install — everything it needs comes along automatically, with nothing native to compile, so it stays small and runs the same way in Node and in the browser.

Which function do I need?

I want to…Use
Combine multiple PDFs into onemergePdfs
Split a PDF into separate pages or specific rangessplitPdf
Rotate every page 90°, 180°, or 270°rotatePdf
Reorder, rotate, or delete specific pagesorganizePages
Mark a document CONFIDENTIAL or DRAFT (visible)addWatermark
Trace a leaked copy back to who received it (invisible)fingerprintPdf
Add page numbersaddPageNumbers
Add a running header/footer, with date and page countaddHeaderFooter
Trim margins, or resize every page to A4/LettercropPdf
Bake a filled-in form's values in and make it read-onlyflattenPdf
See what author/app/timestamp metadata a PDF is carryingscanPdfMetadata
Remove that metadata before sharing a filestripPdfMetadata
Turn a CSV export into a paginated PDF tablecsvToPdf

This table is the gap a bare API reference leaves — the full signatures, options, and examples for every function below.

Function reference

mergePdfs(files: Uint8Array[]): Promise<Uint8Array>

Merge multiple PDFs into one, in the given order.

import { mergePdfs } from "offlinepdf-sdk";
import fs from "node:fs";

const a = fs.readFileSync("a.pdf");
const b = fs.readFileSync("b.pdf");
const merged = await mergePdfs([a, b]);
fs.writeFileSync("merged.pdf", merged);

splitPdf(file: Uint8Array, options?: SplitOptions): Promise<{ name: string; bytes: Uint8Array }[]>

Split a PDF into individual pages, or into ranges. options.ranges is a string like"1-3, 5, 7-10"; omit it to get one file per page. Returns a single file when the result is one PDF, or a split.zip entry when there are several.

import { splitPdf } from "offlinepdf-sdk";

// One PDF per page, zipped:
const parts = await splitPdf(file);

// A specific range, as a single PDF:
const [range] = await splitPdf(file, { ranges: "1-3, 5" });

rotatePdf(file: Uint8Array, angle?: 90 | 180 | 270): Promise<Uint8Array>

Rotate every page by the given angle. Rotation is additive — a page that's already rotated keeps its existing orientation and gets another turn. Defaults to 90.

import { rotatePdf } from "offlinepdf-sdk";

const rotated = await rotatePdf(file, 180);

organizePages(file: Uint8Array, ops: PageOp[]): Promise<Uint8Array>

Reorder, rotate, and drop pages in one pass. ops is an explicit allowlist: each entry is { originalIndex, rotation }, and the output contains exactly those pages, in that order.

This is deletion-by-omission, not a partial patch. Any page whose index isn't in ops is removed — it does not pass through unchanged. To rotate or reorder without deleting anything, every original page index must appear exactly once in ops.

import { organizePages } from "offlinepdf-sdk";

// Keep page 1 as-is, then page 3 rotated 90°. Page 2 (and anything beyond page 3) is dropped.
const out = await organizePages(file, [
  { originalIndex: 0, rotation: 0 },
  { originalIndex: 2, rotation: 90 },
]);

addWatermark(file: Uint8Array, options?: WatermarkOptions): Promise<Uint8Array>

Stamp a text or image watermark on every page. Options: text (default "CONFIDENTIAL"), opacity (0–1), rotation in degrees,fontSize, or an image (JPEG/PNG bytes) to stamp a logo instead of text.

import { addWatermark } from "offlinepdf-sdk";

const out = await addWatermark(file, {
  text: "CONFIDENTIAL",
  opacity: 0.3,
  rotation: 45,
  fontSize: 48,
});

// Or stamp a logo instead of text:
const withLogo = await addWatermark(file, { image: logoBytes });

addPageNumbers(file: Uint8Array, options?: PageNumberOptions): Promise<Uint8Array>

Add page numbers to every page. format is "n", "n/N", or "Page n"; position picks a corner; skipFirst skips a cover page.

import { addPageNumbers } from "offlinepdf-sdk";

const out = await addPageNumbers(file, {
  format: "n/N",            // "n" | "n/N" | "Page n"
  position: "bottom-right", // "bottom-center" | "bottom-left" | "bottom-right" | "top-center"
  skipFirst: true,
});

addHeaderFooter(file: Uint8Array, options?: HeaderFooterOptions): Promise<Uint8Array>

Add a header and/or footer to every page, with an optional date and page number. A semi-transparent white band is drawn behind the text so it stays readable on dark or full-bleed pages.

import { addHeaderFooter } from "offlinepdf-sdk";

const out = await addHeaderFooter(file, {
  header: "Q3 Report",
  footer: "Confidential",
  includePageNumber: true,
  includeDate: true,
});

cropPdf(file: Uint8Array, options?: CropOptions): Promise<Uint8Array>

Crop margins by percentage (mode: "margins", the default), or resize every page to a target size (mode: "resize", with target of "a4", "letter", or "keep", and fit of "contain" or "stretch").

import { cropPdf } from "offlinepdf-sdk";

// Trim a 5% margin on every side:
const trimmed = await cropPdf(file, {
  marginTop: 0.05, marginRight: 0.05, marginBottom: 0.05, marginLeft: 0.05,
});

// Resize every page to A4, preserving aspect ratio:
const resized = await cropPdf(file, { mode: "resize", target: "a4", fit: "contain" });

fingerprintPdf(file: Uint8Array, options?: FingerprintOptions): Promise<{ bytes: Uint8Array; id: string }>

Stamp a PDF with a unique ID: a deterrent against leaks, not forensic-grade tracking. The ID sits in the metadata and as near-invisible text on every page, so a leaked copy can be matched back to who received it. A print-to-PDF, flatten, or screenshot can remove it.

Return shape is different here. Every other function above returns bare bytes; this one returns { bytes, id }, because the generated ID is the whole point of running it and isn't recoverable from the file afterward — you need to save it yourself.

import { fingerprintPdf } from "offlinepdf-sdk";

const { bytes, id } = await fingerprintPdf(file, { label: "sent to Acme Corp" });
console.log("fingerprint ID:", id); // write this down — it's shown once and stored nowhere

flattenPdf(file: Uint8Array): Promise<Uint8Array>

Flatten interactive form fields into static page content and strip annotations, so filled-in values stay visible but can no longer be edited.

import { flattenPdf } from "offlinepdf-sdk";

const out = await flattenPdf(file);

scanPdfMetadata(file: Uint8Array): Promise<{ findings: MetadataFinding[]; pageCount: number }>

Scan a PDF for author, creator app, timestamps, and other metadata, without modifying it. Read-only — pair with stripPdfMetadata below once you know what's there.

import { scanPdfMetadata } from "offlinepdf-sdk";

const { findings, pageCount } = await scanPdfMetadata(file);
for (const f of findings) console.log(f.key, f.value, f.risk); // risk: "low" | "medium" | "high"

stripPdfMetadata(file: Uint8Array): Promise<{ bytes: Uint8Array; findings: MetadataFinding[] }>

Strip the metadata scanPdfMetadata finds — title, author, subject, keywords, creator/producer app fields, creation/modification dates — plus page-level/Metadata, /PieceInfo, and FileAttachment annotations (and their embedded-file streams) that a naive page copy would otherwise silently carry over. Verified at the byte level: the test suite builds a PDF with hidden XMP, an embedded file, and a file-attachment annotation, runs the strip, then inspects every indirect object of the output to confirm nothing survives.

Return shape is different here too. Like fingerprintPdf, this returns { bytes, findings } rather than bare bytes, so you can show the caller what was removed.

import { stripPdfMetadata } from "offlinepdf-sdk";

const { bytes, findings } = await stripPdfMetadata(file);
console.log("removed:", findings.map((f) => f.key));

csvToPdf(file: Uint8Array, options?: CsvToPdfOptions): Promise<Uint8Array>

Convert a CSV file into a paginated PDF table. The first row becomes a bold header repeated on every page; remaining rows continue onto new A4 pages instead of one endless page. Handles quoted fields with embedded commas. Supports up to 20,000 rows and 40 columns.

import { csvToPdf } from "offlinepdf-sdk";

const out = await csvToPdf(file, { title: "Q3 Export" });

Limits

Each function enforces the same sane safety caps the website uses: PDFs over 750 pages are rejected, and an operation that would produce output over 750 MB throws rather than exhausting memory. These aren't configurable yet.

Not yet included

This package covers 12 of OfflinePDF's 42 tools. Compress, Encrypt PDF, Remove Password, OCR, Images to PDF, and the other 30 tools on the website are not in this package — most depend on WASM binaries (qpdf for encryption) or browser-only APIs (OffscreenCanvas for image compression and format conversion) that don't belong in a lightweight, zero-native-dependency package. Images to PDF is a deliberate exclusion rather than an oversight: its JPEG/PNG path is clean, but the website tool also accepts WebP/GIF via browser canvas, and shipping a version with quietly narrower format support than the name implies is worse than not shipping it — same reasoning as Compress and Encrypt. These may ship as separate add-on packages later; this page and the npm README will stay honest about what's actually in the box rather than imply more than these 12 functions.

For everything else — compress, convert, OCR, redact, and the rest — use the full site, free, no account, nothing uploaded.

Links

npm package · source on GitHub