# Node.js SDK: Generate Documents from JavaScript


We provide a [Node.js SDK](https://github.com/pdfmonkey/pdfmonkey-node) to connect to PDFMonkey. This package is the quickest way to use our API from JavaScript or TypeScript.

- npm: [https://www.npmjs.com/package/pdfmonkey](https://www.npmjs.com/package/pdfmonkey)
- GitHub: [https://github.com/pdfmonkey/pdfmonkey-node](https://github.com/pdfmonkey/pdfmonkey-node)

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

```bash
$ npm install pdfmonkey
```

Or with your package manager of choice:

```bash
$ 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.

```bash
PDFMONKEY_API_KEY=j39ckj4…
```

With the variable set, you can create a client without any argument:

```ts
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:

```ts
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:

```ts
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](../api/documents.md#the-documentcard-object).

```ts
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:

```ts
const card = await client.documents.generateSync(
  { document_template_id: "…", payload: { name: "John Doe" } },
  { timeout: 300_000 },
);
```

> [!WARNING]
> **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:
> 
> ```ts
> 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](../generating-documents/webhooks.md) 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'`:

```ts
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](../generating-documents/webhooks.md) 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:

```ts
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.

> [!TIP]
> This is useful for embedding a preview via `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`.


```ts
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:

```ts
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](../generating-documents/custom-filename.md) and [PDF Password Protection](../generating-documents/pdf-password-protection.md) 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:

```ts
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:

```ts
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:

```ts
// 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

```ts
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`:

```ts
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

> [!CAUTION]
> **Prefer documentCards.get over documents.get**
>
> Fetching a full document includes its payload, meaning it could be large depending on the data you provided. We **strongly** recommend using `client.documentCards.get` unless you have a specific reason to fetch the full document.


To fetch the lightweight card representation (recommended):

```ts
const card = await client.documentCards.get(
  "76bebeb9-9eb1-481a-bc3c-faf43dc3ac81",
);
```

To fetch the full document, including its payload:

```ts
const document = await client.documents.get(
  "76bebeb9-9eb1-481a-bc3c-faf43dc3ac81",
);
```

#### Deleting a document

```ts
await client.documents.delete("76bebeb9-9eb1-481a-bc3c-faf43dc3ac81");
```

### Error handling

API errors and network errors throw typed exceptions:

```ts
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:

```ts
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](#configuration) to adjust this.

### Templates

#### Fetching a template

> [!CAUTION]
> **Full templates can be large**
>
> Fetching a full template includes its body and settings, which can be large. Use `list` when you only need metadata.


```ts
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`):

```ts
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](#engines) if you need to pin a specific version.

#### Updating a template

Like `create`, `update` writes to the draft fields:

```ts
const updated = await client.documentTemplates.update(template.id, {
  body_draft: "<h1>Updated Invoice</h1>",
});
updated.body_draft; // => '<h1>Updated Invoice</h1>'
```

#### Listing templates

```ts
const page = await client.documentTemplates.list({
  workspace_id: "f4ab650c-…",
});
```

#### Deleting a template

```ts
await client.documentTemplates.delete("b13ebd75-…");
```

### Template Folders

```ts
// 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`:

```ts
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.

```ts
// 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.

```ts
// 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:

```ts
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:

```ts
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:

```ts
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:

```ts
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);
```

> [!NOTE]
> `page.data` only contains the items **of the current page**. To process all pages, navigate manually:


```ts
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:

```ts
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:

```ts
await client.documents.create(
  { document_template_id: "b13ebd75-…", payload: { invoice: 1 } },
  {
    signal: AbortSignal.timeout(10_000),
    timeout: 60_000,
    maxRetries: 0,
  },
);
```


## FAQ

**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.
