Node.js SDK: Generate PDFs from JavaScript and TypeScript
Last updated August 20, 2026
We provide a Node.js SDK to connect to PDFMonkey. This package is the quickest way to use our API from JavaScript or TypeScript.
The SDK has zero runtime dependencies, ships both ESM and CommonJS builds, and is fully typed. It only relies on the global fetch API and Web Crypto, so it runs on Node.js 20+, Bun, Deno, and edge runtimes such as Cloudflare Workers or Vercel Edge Functions.
Installation #
$ npm install pdfmonkey
Or with your package manager of choice:
$ pnpm add pdfmonkey
$ yarn add pdfmonkey
$ bun add pdfmonkey
Usage #
Setting up authentication #
Using the default environment variable #
The SDK looks for the PDFMONKEY_API_KEY environment variable. This variable should contain your API key obtained at https://dashboard.pdfmonkey.io/account.
PDFMONKEY_API_KEY=j39ckj4…
With the variable set, you can create a client without any argument:
import { PDFMonkey } from "pdfmonkey";
const client = new PDFMonkey();
Setting credentials manually #
You can also pass the API key explicitly, either as a string or inside an options object:
const client = new PDFMonkey("j39ckj4…");
// or, with extra options
const client = new PDFMonkey({
apiKey: "j39ckj4…",
timeout: 30_000,
});
Per-tenant credentials #
Credentials are scoped to a client instance. If you need per-request credentials (e.g. multi-tenant scenarios), create one client per tenant:
const tenantClient = new PDFMonkey(tenant.pdfmonkeyApiKey);
const card = await tenantClient.documents.generateSync({
document_template_id: "b13ebd75-…",
payload: { name: "John Doe" },
});
Documents #
Synchronous generation #
If you want to wait for a document’s generation before continuing with your workflow, use generateSync. It requests a document generation and waits for it to succeed or fail before returning a DocumentCard.
const card = await client.documents.generateSync({
document_template_id: "b13ebd75-d290-409b-9cac-8f597ae3e785",
payload: { name: "John Doe" },
});
card.status; // => 'success'
card.download_url; // => 'https://…'
The request times out after 2 minutes by default. You can override it with the timeout option:
const card = await client.documents.generateSync(
{ document_template_id: "…", payload: { name: "John Doe" } },
{ timeout: 300_000 },
);
The download URL is temporary
The download URL of a document is only valid for 1 hour. Past this delay, fetch the document card again to obtain a new one:
const fresh = await client.documentCards.get(card.id);
fresh.download_url; // => new URL, valid for 1 hour
Asynchronous generation #
PDFMonkey was created with an asynchronous workflow in mind. It provides webhooks to inform you of a document’s generation success or failure.
To leverage this behavior and continue working while your document is being generated, create the document with status: 'pending':
const document = await client.documents.create({
document_template_id: "b13ebd75-d290-409b-9cac-8f597ae3e785",
payload: { name: "John Doe" },
status: "pending",
});
document.status; // => 'pending'
document.download_url; // => null
If you have a webhook URL set up, it will be called with your document once the generation is complete. See the Webhooks page for the payload format and signature verification.
If you would rather poll than rely on webhooks, waitForGeneration polls the API until the document reaches a final status:
const completed = await client.documents.waitForGeneration(document.id, {
interval: 2000, // initial poll interval in ms (default: 2000)
maxInterval: 10_000, // cap for the exponential backoff (default: 10 000)
timeout: 120_000, // give up after this many ms (default: 120 000)
signal: AbortSignal.timeout(60_000), // optional AbortSignal
});
completed.status; // => 'success'
completed.download_url; // => 'https://…'
It throws a PDFMonkeyError if the document ends in failure or error, or if the timeout is reached.
Draft documents #
You can create a draft document that won’t be queued for generation. This is the default when status is omitted.
preview_url before triggering generation. That’s what we do in the PDFMonkey dashboard to show you a preview of your document before generating it, using an iframe.const draft = await client.documents.create({
document_template_id: "b13ebd75-d290-409b-9cac-8f597ae3e785",
payload: { name: "John Doe" },
});
draft.status; // => 'draft'
draft.preview_url; // => 'https://…'
// When ready, trigger generation:
const pending = await client.documents.update(draft.id, { status: "pending" });
pending.status; // => 'pending'
// Then wait for completion:
const completed = await client.documents.waitForGeneration(draft.id);
completed.status; // => 'success'
Attaching meta data #
In addition to the document’s payload you can add meta data when generating a document. Pass the meta property to generateSync, create, or update. It accepts either an object or a pre-serialized JSON string:
const card = await client.documents.generateSync({
document_template_id: templateId,
payload: payload,
meta: {
_filename: "john-doe-contract.pdf", // sets the download filename
_password: "secret123", // encrypts the PDF (AES-256)
client_id: "123xxx123", // your own metadata
},
});
card.meta;
// => '{"_filename":"john-doe-contract.pdf","_password":"secret123","client_id":"123xxx123"}'
See Custom Filename and PDF Password Protection for details on the _filename and _password keys.
meta is returned as a JSON string by the API. Use parseMeta to recover the structured object you sent:
import { parseMeta } from "pdfmonkey";
const meta = parseMeta(card.meta); // DocumentMeta | null
meta?._filename; // => 'john-doe-contract.pdf'
meta?.client_id; // => '123xxx123'
Image generation #
Image generation uses the same API flow as PDF generation. The template’s output_type attribute indicates whether it produces 'pdf' or 'image' output. Image-specific options are passed through the meta property:
const card = await client.documents.generateSync({
document_template_id: templateId,
payload: payload,
meta: {
_type: "png", // webp (default), png, or jpg
_width: 800, // pixels
_height: 600, // pixels
_quality: 80, // webp only, default 100
},
});
card.download_url; // => URL to the generated image
Downloading the file #
Instead of fetching download_url yourself, let the SDK do it. Pass a document, a document card, or an ID:
// As a Uint8Array
const bytes = await client.documents.download(card);
await fs.promises.writeFile("contract.pdf", bytes);
// As a ReadableStream — pipe straight to disk or an HTTP response
const stream = await client.documents.downloadStream(card.id);
Both helpers throw a PDFMonkeyError if the document has no download_url yet — wait for generation to complete first.
Updating a document #
const updated = await client.documents.update(document.id, {
payload: { name: "Jane Doe" },
status: "pending",
});
Listing documents #
Listing returns lightweight document cards, so it lives on client.documentCards:
const page = await client.documentCards.list({ page: 1, status: "success" });
for (const card of page.data) {
console.log(card.id, card.status);
}
page.currentPage; // => 1
page.totalPages; // => 5
// Navigate pages
if (page.hasNextPage()) {
const next = await page.getNextPage();
}
You can filter by document_template_id, status, workspace_id, and updated_since.
Fetching a document #
Prefer documentCards.get over documents.get
client.documentCards.get unless you have a specific reason to fetch the full document.To fetch the lightweight card representation (recommended):
const card = await client.documentCards.get(
"76bebeb9-9eb1-481a-bc3c-faf43dc3ac81",
);
To fetch the full document, including its payload:
const document = await client.documents.get(
"76bebeb9-9eb1-481a-bc3c-faf43dc3ac81",
);
Deleting a document #
await client.documents.delete("76bebeb9-9eb1-481a-bc3c-faf43dc3ac81");
Error handling #
API errors and network errors throw typed exceptions:
import {
APIConnectionError,
APIError,
AuthenticationError,
NotFoundError,
RateLimitError,
UnprocessableEntityError,
} from "pdfmonkey";
try {
await client.documents.create({
document_template_id: templateId,
payload: data,
});
} catch (error) {
if (error instanceof AuthenticationError) {
// Invalid API key (401)
} else if (error instanceof NotFoundError) {
// Resource not found (404)
} else if (error instanceof UnprocessableEntityError) {
error.body; // => { errors: { document_template_id: ["can't be blank"] } }
} else if (error instanceof RateLimitError) {
error.retryAfter; // => seconds to wait, from the Retry-After header
} else if (error instanceof APIError) {
error.status; // => any other HTTP error status
} else if (error instanceof APIConnectionError) {
error.cause; // => original network error
}
}
All exception classes inherit from PDFMonkeyError, so you can catch broadly:
import { PDFMonkeyError } from "pdfmonkey";
try {
await client.documents.generateSync({
document_template_id: templateId,
payload: data,
});
} catch (error) {
if (error instanceof PDFMonkeyError) {
console.error(`Something went wrong: ${error.message}`);
}
}
The client automatically retries requests that fail with a 408, 429, or 5xx status (2 retries by default, with exponential backoff honoring the Retry-After header). See Configuration to adjust this.
Templates #
Fetching a template #
Full templates can be large
list when you only need metadata.const template = await client.documentTemplates.get(
"b13ebd75-d290-409b-9cac-8f597ae3e785",
);
template.identifier; // => 'my-invoice'
template.body; // => '<h1>Invoice</h1>…' (published version)
template.body_draft; // => '<h1>Invoice v2</h1>…' (draft version)
Creating a template #
When creating a template, write to the draft fields (body_draft, scss_style_draft, sample_data_draft, settings_draft):
const template = await client.documentTemplates.create({
identifier: "my-invoice",
body_draft: "<h1>Invoice</h1>",
});
template.body_draft; // => '<h1>Invoice</h1>'
Leave pdf_engine_draft_id unset: the API automatically selects the latest engine. See Engines if you need to pin a specific version.
Updating a template #
Like create, update writes to the draft fields:
const updated = await client.documentTemplates.update(template.id, {
body_draft: "<h1>Updated Invoice</h1>",
});
updated.body_draft; // => '<h1>Updated Invoice</h1>'
Listing templates #
const page = await client.documentTemplates.list({
workspace_id: "f4ab650c-…",
});
Deleting a template #
await client.documentTemplates.delete("b13ebd75-…");
Template Folders #
// List folders
const folders = await client.templateFolders.list();
// Create a folder
const folder = await client.templateFolders.create({ identifier: "invoices" });
// Fetch a folder
const folder = await client.templateFolders.get("folder-id");
// Update a folder
await client.templateFolders.update("folder-id", { identifier: "receipts" });
// Delete a folder
await client.templateFolders.delete("folder-id");
To create a template inside a specific folder, pass the template_folder_id:
const folder = await client.templateFolders.create({ identifier: "invoices" });
const template = await client.documentTemplates.create({
identifier: "monthly-invoice",
body_draft: "<h1>Invoice</h1>",
template_folder_id: folder.id,
});
Snippets #
Snippets are reusable HTML components that can be included in templates.
// List snippets
const snippets = await client.snippets.list();
// Create a snippet
const snippet = await client.snippets.create({
identifier: "header",
code: '<div class="header">…</div>',
workspace_id: "f4ab650c-…",
});
// Fetch a snippet
const snippet = await client.snippets.get("snippet-id");
// Update a snippet
await client.snippets.update("snippet-id", {
code: '<div class="header">Updated</div>',
});
// Delete a snippet
await client.snippets.delete("snippet-id");
Workspaces #
Workspaces are read-only resources. They can be listed and fetched but not created, updated, or deleted through the API.
// List workspaces
const page = await client.workspaces.list();
for (const workspace of page.data) {
console.log(workspace.identifier);
}
// Fetch a workspace
const workspace = await client.workspaces.get("workspace-id");
workspace.identifier; // => 'my-app'
Engines #
List available PDF rendering engines:
const engines = await client.pdfEngines.list();
for (const engine of engines) {
console.log(
`${engine.name} v${engine.version} (deprecated: ${engine.deprecated_on ?? "no"})`,
);
}
Most integrations don’t need this: the API picks the latest engine for new templates. Use it only to pin a template to a specific engine version:
const engines = await client.pdfEngines.list();
const chromium = engines.find((engine) => engine.name === "chromium");
await client.documentTemplates.update(template.id, {
pdf_engine_draft_id: chromium.id,
});
Current User #
Retrieve information about the authenticated user:
const user = await client.currentUser.get();
user.email; // => 'user@example.com'
user.current_plan; // => 'pro'
user.available_documents; // => 1000
Pagination #
All list methods return a Page<T> object with built-in navigation:
const page = await client.documentCards.list({ page: 1 });
page.data; // => items on this page
page.currentPage; // => 1
page.totalPages; // => 5
// Navigate to next/previous pages
if (page.hasNextPage()) {
const next = await page.getNextPage();
}
if (page.hasPreviousPage()) {
const prev = await page.getPreviousPage();
}
// Jump to a specific page
const last = await page.getPage(page.totalPages);
page.data only contains the items of the current page. To process all pages, navigate manually:let page = await client.documentTemplates.list();
while (true) {
for (const template of page.data) {
process(template);
}
if (!page.hasNextPage()) break;
page = await page.getNextPage();
}
Configuration #
All options are optional. Pass them to the constructor:
const client = new PDFMonkey({
apiKey: "j39ckj4…", // or set PDFMONKEY_API_KEY in the environment
baseURL: "https://api.pdfmonkey.io/api/v1", // default
timeout: 30_000, // request timeout in ms (default: 30s)
maxRetries: 2, // retry on 408/429/5xx (default: 2)
fetch: customFetch, // bring your own fetch implementation
logger: console, // debug logging
defaultHeaders: { "X-Source": "my-app" }, // sent with every request
retryDelay: (attempt) => attempt * 250, // custom backoff strategy
hooks: {
// request/response/error interceptors
onRequest: (ctx) => {
ctx.headers["X-Trace-Id"] = newTraceId();
},
onResponse: (ctx) => metrics.observe(ctx.durationMs, ctx.response.status),
},
});
Per-request options #
Every resource method accepts a trailing options object to override signal, timeout, and maxRetries for a single call:
await client.documents.create(
{ document_template_id: "b13ebd75-…", payload: { invoice: 1 } },
{
signal: AbortSignal.timeout(10_000),
timeout: 60_000,
maxRetries: 0,
},
);
Frequently asked questions
- How do I install the PDFMonkey Node.js SDK?
- Run npm install pdfmonkey (or the equivalent with pnpm, yarn, or bun). Set your API key via the PDFMONKEY_API_KEY environment variable or pass it to the PDFMonkey constructor. The package has zero runtime dependencies and ships both ESM and CommonJS builds.
- What is the difference between generateSync and create in the PDFMonkey Node.js SDK?
- generateSync sends a single request and waits for the document to finish generating, returning a DocumentCard with a download URL. create returns immediately—with status 'pending' it queues generation and you handle the result via webhooks or waitForGeneration; with status 'draft' (the default) nothing is generated until you update the status.
- Can I use the PDFMonkey Node.js SDK in edge runtimes like Cloudflare Workers or Vercel Edge?
- Yes. The SDK only relies on the global fetch API and Web Crypto, so it runs on Node.js 20+, Bun, Deno, Cloudflare Workers, and Vercel Edge Functions. You can pass a custom fetch implementation if your runtime needs a wrapped one.
- How do I handle errors in the PDFMonkey Node.js SDK?
- API errors throw typed subclasses of APIError such as AuthenticationError (401), NotFoundError (404), UnprocessableEntityError (422), and RateLimitError (429, with a retryAfter property). Network failures throw APIConnectionError. All errors inherit from PDFMonkeyError for broad catching.