@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.
Maintainers
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.tstypes - Automatic retries with exponential backoff and
Retry-Aftersupport - Per-request timeouts and
AbortSignalcancellation - 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-sdkQuick 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 onlyconvertPdfToDocx(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 onlyA 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 linkEdit: 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"); // stringconvert() 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,504responses are retried up tomaxRetriestimes (default 3) with exponential backoff and jitter (0.25–8 s). ARetry-Afterheader is honoured (capped at 60 s).4xxerrors other than 429 are never retried. A503whosedetailsays a feature is missing on the server (not installed,not configured,not available) is not retried either; a busy503carryingRetry-Afteris. SetmaxRetries: 0to disable. - Timeouts: each attempt is aborted after
timeoutMs(default 300 000 ms, matching the server's nginx read timeout) and fails withTimeoutError. 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 buildLicense
MIT © Rahul Raj
