npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@iamrraj/converter-sdk

v0.2.1

Published

TypeScript SDK for the iamrraj Converter API: office conversions, PDF tools (merge, split, compress, watermark, OCR, sign, redact, forms), HTML to PDF and GeoIP lookups.

Readme

@iamrraj/converter-sdk

TypeScript client for the Converter API: office conversions (Word, PowerPoint, Excel, OpenDocument, Markdown), a full PDF toolbox (merge, split, compress, watermark, redact, protect, forms, OCR, digital signatures, text extraction), HTML to PDF and GeoIP lookups. Works in Node.js 18+ and modern browsers with zero runtime dependencies (global fetch, FormData, Blob and DecompressionStream).

  • ESM and CommonJS builds with bundled .d.ts types
  • Automatic retries with exponential backoff and Retry-After support
  • Per-request timeouts and AbortSignal cancellation
  • Typed responses and a typed error hierarchy
  • Client-side validation of extensions, page specs and numeric ranges before upload
  • Zip responses unpacked in memory, no dependencies
  • The API key is never logged, serialised or exposed

Install

npm install @iamrraj/converter-sdk

Quick start

Node (ESM)

import { ConverterClient } from "@iamrraj/converter-sdk";

const client = new ConverterClient({ apiKey: process.env.CONVERTER_API_KEY });

const result = await client.convertDocToPdf("./report.docx");
await result.save("./report.pdf");

const merged = await client.pdf.merge(["./a.pdf", "./b.pdf"]);
await merged.save("./merged.pdf");

const pages = await client.pdf.toImages("./deck.pdf", { fmt: "png", dpi: 72 });
await pages.saveAll("./deck-pages");

Node (CommonJS)

const { ConverterClient } = require("@iamrraj/converter-sdk");

const client = new ConverterClient({ apiKey: process.env.CONVERTER_API_KEY });

client.convertHtmlToPdf("<h1>Hello</h1>").then((result) => result.save("hello.pdf"));

Browser

<script type="module">
  import { ConverterClient } from "https://esm.sh/@iamrraj/converter-sdk";

  const client = new ConverterClient({ apiKey: "your-key" });
  const status = await client.health();
  console.log(status);
</script>

Do not ship a privileged API key to browsers you do not control. convertDocToPdf needs no key, so it can be called from any page; proxy the other endpoints through your own backend when the key must stay secret.

Configuration

const client = new ConverterClient({
  apiKey: "...", // default: process.env.CONVERTER_API_KEY (Node only)
  baseUrl: "https://fastapi.iamrraj.com", // default: process.env.CONVERTER_BASE_URL or production
  timeoutMs: 300_000, // per attempt, default 5 minutes
  maxRetries: 3, // retries on network errors and 429/502/503/504
  fetch: globalThis.fetch, // inject a polyfill or a test double
  userAgent: "my-app/1.0", // sent as User-Agent in Node
  onRequest: ({ method, url, attempt }) => {},
  onResponse: ({ status, durationMs }) => {},
});

Environment variables (Node)

| Variable | Purpose | | -------------------- | ---------------------------------------------------- | | CONVERTER_API_KEY | Used when apiKey is not passed to the constructor. | | CONVERTER_BASE_URL | Used when baseUrl is not passed. |

Both are read only when process exists, so browser bundles are unaffected.

Methods

Every method accepts an optional signal (an AbortSignal) and timeoutMs (per-attempt timeout for that call) in its options. Methods that upload a file also accept filename for inputs that carry no name.

health()

const { status, timestamp } = await client.health();

Public endpoint; no API key required.

convertDocToPdf(file, options?)

Converts .doc / .docx to PDF. Public endpoint; no API key required.

const result = await client.convertDocToPdf(new File([bytes], "letter.docx"));
const result = await client.convertDocToPdf(bytes, { filename: "letter.docx" });
const result = await client.convertDocToPdf({ data: blob, filename: "letter.docx" });
const result = await client.convertDocToPdf("./letter.docx"); // Node only

convertPdfToDocx(file, options?)

Converts .pdf to DOCX. Requires an API key.

const result = await client.convertPdfToDocx("./scan.pdf");
console.log(result.filename); // "scan.docx"

convertHtmlToPdf(html, options?)

Renders an HTML string to PDF. Requires an API key.

const result = await client.convertHtmlToPdf("<h1>Invoice #42</h1>");
const pdfBytes = await result.bytes();

convert(file, { to })

Routes by file extension: .doc/.docx with to: "pdf", or .pdf with to: "docx". Any other pair throws UnsupportedConversionError before any network call.

const result = await client.convert(file, { to: "pdf" });

File inputs

type FileInput =
  | File
  | Blob
  | ArrayBuffer
  | Uint8Array
  | { data: Blob | ArrayBuffer | Uint8Array; filename: string }
  | string; // filesystem path, Node only

A filename is required whenever the input does not carry one (Blob, ArrayBuffer, Uint8Array); pass it via { filename } in the options. The extension is validated client-side and InvalidFileTypeError is thrown before anything is uploaded.

ConversionResult

interface ConversionResult {
  blob: Blob;
  filename: string; // from Content-Disposition, with a fallback
  mediaType: string; // e.g. "application/pdf"
  size: number; // bytes
  arrayBuffer(): Promise<ArrayBuffer>;
  bytes(): Promise<Uint8Array>;
  save(path?: string): Promise<string>; // Node only; defaults to `filename` in cwd
}

save() throws EnvironmentError in browsers; use an object URL instead (see the React example below).

geoip(ip), geoipMe(), geoipStatus()

import { coordinates } from "@iamrraj/converter-sdk";

const info = await client.geoip("8.8.8.8");
console.log(info.country, info.city, coordinates(info)); // "United States" "Mountain View" [37.422, -122.085]

const me = await client.geoipMe(); // geolocates the caller's public IP
const status = await client.geoipStatus(); // { status: "ok" | "unavailable", cityDb, asnDb, ... }

GeoIPResult fields other than ip, ipVersion and attribution are typed | null because the database may not know them. coordinates() returns [latitude, longitude] or null.

PDF toolbox: client.pdf

All /pdf/* endpoints live on client.pdf. Each method validates the file extension (.pdf), page specs and numeric ranges before uploading; failures throw InvalidFileTypeError or InvalidArgumentError without a network call.

Page specs are strings: "all", "1", "1,3-5", "2-last", reverse ranges like "5-2". Omitted means all pages.

Pages: merge, split, rotate, crop, reorder, delete, extract, info, numbers, resize

// Node
const merged = await client.pdf.merge(["./a.pdf", "./b.pdf", "./c.pdf"], { order: [3, 1, 2] });

const parts = await client.pdf.split("./book.pdf", { mode: "ranges", ranges: ["1-3", "4-last"] });
parts.isArchive; // true when the server answered with a zip
parts.files.map((f) => f.filename); // ["book-1.pdf", "book-2.pdf"]
await parts.saveAll("./out"); // Node only

await client.pdf.rotate("./scan.pdf", { degrees: -90, pages: "2-last" });
await client.pdf.crop("./scan.pdf", { left: 10, right: 10, unit: "mm" });
await client.pdf.reorder("./deck.pdf", "3,1,2");
await client.pdf.deletePages("./deck.pdf", "2,4-6");
await client.pdf.extractPages("./deck.pdf", "1,1,3-2");
await client.pdf.pageNumbers("./deck.pdf", {
  position: "bottom-right",
  format: "Page {n} / {total}",
});
await client.pdf.resize("./deck.pdf", { size: "A4", orientation: "auto" });

const info = await client.pdf.info("./deck.pdf");
info.pageCount; // 12
info.metadata.title; // string | null
info.pages[0]; // { number: 1, widthPt: 595.2, heightPt: 841.9, rotation: 0 }
// Browser: <input type="file" id="pdf" multiple>
const input = document.querySelector<HTMLInputElement>("#pdf");
const files = [...(input?.files ?? [])];
const merged = await client.pdf.merge(files);
const url = URL.createObjectURL(merged.blob); // offer as a download link

Edit: compress, repair, watermarks, metadata

const small = await client.pdf.compress("./big.pdf", { level: "high" });
small.originalSize; // from X-Original-Size
small.compressedSize; // from X-Compressed-Size
small.ratio; // compressedSize / originalSize, or null

await client.pdf.repair("./damaged.pdf");
await client.pdf.watermarkText("./doc.pdf", "DRAFT", { opacity: 0.2, position: "diagonal" });
await client.pdf.watermarkImage("./doc.pdf", "./logo.png", { scale: 0.3, layer: "under" });
await client.pdf.metadataSet("./doc.pdf", { title: "Report", author: "Ada" }); // at least one field
await client.pdf.metadataStrip("./doc.pdf");

Security: protect, unlock, redact

await client.pdf.protect("./doc.pdf", {
  userPassword: "open-me",
  ownerPassword: "owner",
  permissions: ["print", "copy"],
  encryption: "aes-256",
});
await client.pdf.unlock("./locked.pdf", "open-me");

const redacted = await client.pdf.redact("./doc.pdf", ["Ada Lovelace", "\\d{3}-\\d{4}"], {
  regex: true,
  wholeWord: false,
  fill: "#000000",
});
redacted.redactionCount; // from X-Redaction-Count

await client.pdf.redactAreas("./doc.pdf", [{ page: 1, x0: 50, y0: 50, x1: 300, y1: 120 }]);

Text: extract, markdown, compare

const plain = await client.pdf.text("./doc.pdf"); // PageText[]  { page, text }
const blocks = await client.pdf.text("./doc.pdf", { layout: "blocks" }); // PageBlocks[]  { page, blocks: Span[] }
const words = await client.pdf.text("./doc.pdf", { layout: "words" }); // PageWords[]   { page, words: Span[] }
// Span = { bbox: [x0, y0, x1, y1], text }

const text = await client.pdf.textPlain("./doc.pdf"); // string, pages separated by \f
const md = await client.pdf.markdown("./doc.pdf");
await md.text(); // "# Title\n..."

const diff = await client.pdf.compare("./v1.pdf", "./v2.pdf");
diff.identical; // boolean
diff.pages[0]; // { page, similarity, addedLines, removedLines }

Forms

const fields = await client.pdf.formFields("./form.pdf");
// [{ name, type: "text" | "checkbox" | ..., value, page, options, required, readonly }]

const filled = await client.pdf.fillForm(
  "./form.pdf",
  { name: "Ada", agree: true },
  { flatten: true },
);

Images: render, extract, build, thumbnail

const pages = await client.pdf.toImages("./deck.pdf", { fmt: "jpg", dpi: 150, quality: 80 });
for (const image of pages.files) console.log(image.filename, image.mediaType);

const embedded = await client.pdf.extractImages("./deck.pdf", { minWidth: 100, minHeight: 100 });

const pdf = await client.pdf.fromImages(["./scan-1.jpg", "./scan-2.jpg"], {
  pageSize: "a4",
  fit: "contain",
});

const thumb = await client.pdf.thumbnail("./deck.pdf", { page: 1, width: 240, fmt: "webp" });
// Browser: show the first rendered page
const pages = await client.pdf.toImages(file, { pages: "1", fmt: "png" });
img.src = URL.createObjectURL(pages.first.blob);

OCR

const languages = await client.pdf.ocrLanguages(); // ["eng", "deu", ...]
const searchable = await client.pdf.ocr("./scan.pdf", { language: "eng+deu", force: false });
const recognised = await client.pdf.ocrText("./scan.pdf", { language: "eng" });
recognised.pages[0]; // { page: 1, text: "..." }

Digital signatures

const signed = await client.pdf.sign("./contract.pdf", "./key.p12", {
  password: "p12-passphrase",
  reason: "Approved",
  location: "Berlin",
  visible: true,
  page: 1,
  box: [50, 50, 300, 120],
});

const verification = await client.pdf.verify("./contract-signed.pdf");
verification.signed; // boolean
verification.signatures[0]; // { field, signer, validSignature, intact, trusted, signingTime, ... }

MultiResult

Returned by split, toImages and extractImages, which answer with a zip when there is more than one output and a single file otherwise. The SDK normalises both:

interface MultiResult {
  files: ConversionResult[]; // always populated; zips are unpacked in memory
  first: ConversionResult; // files[0]; throws InvalidResponseError on an empty archive
  isArchive: boolean;
  archive?: ConversionResult; // the zip as sent, when isArchive
  saveAll(dir: string): Promise<string[]>; // Node only; creates dir
}

Deflated zip entries are inflated with DecompressionStream("deflate-raw"); on a runtime without it EnvironmentError is thrown.

Office conversions: client.office

await client.office.wordToPdf("./letter.docx"); // .doc .docx .odt .rtf .txt .md
await client.office.powerpointToPdf("./deck.pptx"); // .ppt .pptx .odp
await client.office.excelToPdf("./data.xlsx"); // .xls .xlsx .ods .csv
await client.office.markdownToPdf("./README.md"); // .md .markdown, styled

await client.office.convert("./letter.docx", { to: "odt" }); // any pair from the matrix
const matrix = await client.office.matrix(); // { ".docx": ["pdf", "docx", "odt", ...], ... }

await client.office.pdfToPowerpoint("./deck.pdf", { pages: "1-5", dpi: 150 });
await client.office.pdfToExcel("./tables.pdf");
await client.office.pdfToHtml("./doc.pdf");
const text = await client.office.pdfToText("./doc.pdf"); // string

convert() checks the pair client-side against OFFICE_MATRIX (Word sources to pdf docx odt rtf txt html, slides to pdf pptx odp, sheets to pdf xlsx ods csv) and throws UnsupportedConversionError listing the allowed targets.

// Browser
const file = input.files?.[0];
if (file) {
  const pdf = await client.office.convert(file, { to: "pdf" });
  link.href = URL.createObjectURL(pdf.blob);
  link.download = pdf.filename;
}

Method reference

| Method | Endpoint | Returns | | ------------------------------------------------ | --------------------------------- | ------------------ | | health() | GET /health | HealthResult | | convertDocToPdf(file) | POST /convert-doc-to-pdf/ | ConversionResult | | convertPdfToDocx(file) | POST /convert-pdf-to-docx/ | ConversionResult | | convertHtmlToPdf(html) | POST /convert-html-to-pdf/ | ConversionResult | | convert(file, { to }) | routes by extension | ConversionResult | | geoip(ip) / geoipMe() / geoipStatus() | GET /geoip... | GeoIPResult | | pdf.merge(files, { order? }) | POST /pdf/merge | ConversionResult | | pdf.split(file, { mode?, ranges? }) | POST /pdf/split | MultiResult | | pdf.rotate(file, { degrees?, pages? }) | POST /pdf/rotate | ConversionResult | | pdf.crop(file, { left?, ..., unit?, pages? }) | POST /pdf/crop | ConversionResult | | pdf.reorder(file, order) | POST /pdf/reorder | ConversionResult | | pdf.deletePages(file, pages) | POST /pdf/pages/delete | ConversionResult | | pdf.extractPages(file, pages) | POST /pdf/pages/extract | ConversionResult | | pdf.info(file) | POST /pdf/info | PdfInfo | | pdf.pageNumbers(file, opts) | POST /pdf/page-numbers | ConversionResult | | pdf.resize(file, { size?, orientation? }) | POST /pdf/resize | ConversionResult | | pdf.compress(file, { level? }) | POST /pdf/compress | CompressResult | | pdf.repair(file) | POST /pdf/repair | ConversionResult | | pdf.watermarkText(file, text, opts) | POST /pdf/watermark/text | ConversionResult | | pdf.watermarkImage(file, image, opts) | POST /pdf/watermark/image | ConversionResult | | pdf.metadataSet(file, values) | POST /pdf/metadata | ConversionResult | | pdf.metadataStrip(file) | POST /pdf/metadata/strip | ConversionResult | | pdf.protect(file, { userPassword, ... }) | POST /pdf/protect | ConversionResult | | pdf.unlock(file, password) | POST /pdf/unlock | ConversionResult | | pdf.redact(file, terms, opts) | POST /pdf/redact | RedactResult | | pdf.redactAreas(file, boxes, { fill? }) | POST /pdf/redact/areas | RedactResult | | pdf.text(file, { pages?, layout? }) | POST /pdf/text | per-layout array | | pdf.textPlain(file, { pages? }) | POST /pdf/text/plain | string | | pdf.markdown(file, { pages? }) | POST /pdf/markdown | MarkdownResult | | pdf.compare(a, b) | POST /pdf/compare | CompareResult | | pdf.formFields(file) | POST /pdf/forms/fields | FormField[] | | pdf.fillForm(file, values, { flatten? }) | POST /pdf/forms/fill | ConversionResult | | pdf.toImages(file, opts) | POST /pdf/to-images | MultiResult | | pdf.extractImages(file, opts) | POST /pdf/extract-images | MultiResult | | pdf.fromImages(files, opts) | POST /pdf/from-images | ConversionResult | | pdf.thumbnail(file, opts) | POST /pdf/thumbnail | ConversionResult | | pdf.ocr(file, opts) | POST /pdf/ocr | ConversionResult | | pdf.ocrText(file, opts) | POST /pdf/ocr/text | OcrResult | | pdf.ocrLanguages() | GET /pdf/ocr/languages | string[] | | pdf.sign(file, certificate, opts) | POST /pdf/sign | ConversionResult | | pdf.verify(file) | POST /pdf/sign/verify | VerifyResult | | office.convert(file, { to }) | POST /convert/office | ConversionResult | | office.matrix() | GET /convert/office/matrix | OfficeMatrix | | office.wordToPdf(file) | POST /convert/word-to-pdf | ConversionResult | | office.powerpointToPdf(file) | POST /convert/powerpoint-to-pdf | ConversionResult | | office.excelToPdf(file) | POST /convert/excel-to-pdf | ConversionResult | | office.pdfToPowerpoint(file, { pages?, dpi? }) | POST /convert/pdf-to-powerpoint | ConversionResult | | office.pdfToExcel(file, { pages? }) | POST /convert/pdf-to-excel | ConversionResult | | office.pdfToHtml(file, { pages? }) | POST /convert/pdf-to-html | ConversionResult | | office.pdfToText(file, { pages? }) | POST /convert/pdf-to-text | string | | office.markdownToPdf(file) | POST /convert/markdown-to-pdf | ConversionResult |

Every option name is the camelCase form of the API's form field (fontSize -> font_size, marginPt -> margin_pt); enums are string literal unions so the compiler catches typos.

Error handling

Every failure is an instance of ConverterError (which extends Error) with a code string for discriminating, plus status, detail (the API's detail field), body, url and method.

| Class | code | When | | ---------------------------- | ------------------------ | ------------------------------------------------------ | | BadRequestError | bad_request | 400, e.g. invalid file type or a non-public IP | | AuthenticationError | authentication_error | 401, missing X-API-Key | | PermissionDeniedError | permission_denied | 403, invalid API key | | NotFoundError | not_found | 404, no location data | | ValidationError | validation_error | 422, errors holds the parsed { loc, msg, type }[] | | RateLimitError | rate_limit | 429, retryAfterMs parsed from Retry-After | | ServiceUnavailableError | service_unavailable | 503 (extends ServerError) | | ServerError | server_error | Any other 5xx | | NetworkError | network_error | fetch failed after all retries | | TimeoutError | timeout | An attempt exceeded timeoutMs | | InvalidFileTypeError | invalid_file_type | Client-side: bad extension or missing filename | | InvalidArgumentError | invalid_argument | Client-side: bad page spec, range, colour or shape | | UnsupportedConversionError | unsupported_conversion | Client-side: convert() / office.convert() pair | | EnvironmentError | environment_error | Node-only feature used in a browser | | InvalidResponseError | invalid_response | 2xx body did not match the documented shape | | ConverterError | converter_error | Any other non-2xx status (e.g. an nginx 413 HTML page) |

Non-JSON error bodies (nginx 413/502 pages) are handled: body holds the raw text and detail a short plain-text summary.

import { ConverterClient, ConverterError, RateLimitError } from "@iamrraj/converter-sdk";

try {
  await client.convertPdfToDocx("./scan.pdf");
} catch (error) {
  if (error instanceof RateLimitError) {
    console.warn(`rate limited, retry in ${error.retryAfterMs ?? 0}ms`);
  } else if (error instanceof ConverterError) {
    console.error(error.code, error.status, error.detail);
  } else {
    throw error;
  }
}

Retries, timeouts and cancellation

  • Retries: network errors and 429, 502, 503, 504 responses are retried up to maxRetries times (default 3) with exponential backoff and jitter (0.25–8 s). A Retry-After header is honoured (capped at 60 s). 4xx errors other than 429 are never retried. A 503 whose detail says a feature is missing on the server (not installed, not configured, not available) is not retried either; a busy 503 carrying Retry-After is. Set maxRetries: 0 to disable.
  • Timeouts: each attempt is aborted after timeoutMs (default 300 000 ms, matching the server's nginx read timeout) and fails with TimeoutError. Pass { timeoutMs } to any method to override it for one call. Timeouts are not retried.
  • Cancellation: pass { signal } to any method. Aborting cancels the in-flight request and any pending retry delay, and the abort reason is rethrown as-is.
const controller = new AbortController();
setTimeout(() => controller.abort(), 10_000);

await client.convertHtmlToPdf(html, { signal: controller.signal });

React example

Upload a file from an <input type="file">, download the result via an object URL. The doc-to-PDF endpoint is public, so no key is needed in the browser.

import { useState } from "react";
import { ConverterClient, ConverterError } from "@iamrraj/converter-sdk";

const client = new ConverterClient();

export function DocToPdf() {
  const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
  const [filename, setFilename] = useState("output.pdf");
  const [error, setError] = useState<string | null>(null);
  const [isConverting, setIsConverting] = useState(false);

  async function onChange(event: React.ChangeEvent<HTMLInputElement>) {
    const file = event.target.files?.[0];
    if (!file) return;
    setIsConverting(true);
    setError(null);
    try {
      const result = await client.convertDocToPdf(file);
      if (downloadUrl) URL.revokeObjectURL(downloadUrl);
      setDownloadUrl(URL.createObjectURL(result.blob));
      setFilename(result.filename);
    } catch (caught) {
      setError(caught instanceof ConverterError ? caught.message : "Conversion failed");
    } finally {
      setIsConverting(false);
    }
  }

  return (
    <div>
      <label>
        Word document
        <input type="file" accept=".doc,.docx" onChange={onChange} disabled={isConverting} />
      </label>
      {isConverting && <p>Converting…</p>}
      {error && <p role="alert">{error}</p>}
      {downloadUrl && (
        <a href={downloadUrl} download={filename}>
          Download {filename}
        </a>
      )}
    </div>
  );
}

Development

npm install
npm run typecheck
npm run lint
npm test -- --coverage
npm run build

License

MIT © Rahul Raj