Official Node.js SDK for the PDFMonkey API. Zero runtime dependencies, dual ESM + CommonJS, Node 20+ and edge-runtime compatible (uses Web Crypto only).
npm install pdfmonkeyimport { PDFMonkey } from 'pdfmonkey';
const client = new PDFMonkey('your-api-key');No
client.documents.list()— listing returns a lightweight summary, so it lives onclient.documentCards.list()(see Pagination). All update endpoints use HTTPPUT(full document replacement) to match the PDFMonkey API contract; the SDK does not currently exposePATCH.
// Create a document (starts as draft)
const doc = await client.documents.create({
document_template_id: 'tpl_xxx',
payload: { name: 'Alice', amount: 42 }, // object — auto-stringified to JSON
status: 'pending', // set to 'pending' to start generation immediately
});
// Get a document
const doc = await client.documents.get('doc_xxx');
// Update a document
const updated = await client.documents.update('doc_xxx', {
payload: { name: 'Bob' },
});
// Delete a document
await client.documents.delete('doc_xxx');Use the meta field to password-protect or set a custom filename on generated PDFs:
const doc = await client.documents.create({
document_template_id: 'tpl_xxx',
payload: { name: 'Alice' },
meta: {
_password: 'secret123', // encrypts the PDF (AES-256)
_filename: 'invoice-42.pdf', // sets the download filename
customField: 'any value', // your own metadata
},
status: 'pending',
});meta accepts either an object (auto-serialized to JSON) or a pre-serialized JSON string. Works on create, update, and generateSync.
// As a Uint8Array
const bytes = await client.documents.download(doc); // or doc.id
// As a ReadableStream — pipe straight to disk or an HTTP response
const stream = await client.documents.downloadStream(doc.id);The helpers throw PDFMonkeyError if the document has no download_url yet — wait for generation to complete first.
Document.meta is a JSON string on the wire. Use parseMeta to recover the structured object you sent:
import { parseMeta } from 'pdfmonkey';
const decoded = parseMeta(doc.meta); // DocumentMeta | null
if (decoded?._filename) console.log(decoded._filename);Generate a PDF and wait for it to complete in a single request:
const card = await client.documents.generateSync({
document_template_id: 'tpl_xxx',
payload: { invoice_number: 1234 },
});
console.log(card.download_url);Create a document then poll until generation completes:
const doc = await client.documents.create({
document_template_id: 'tpl_xxx',
payload: { data: 'value' },
status: 'pending',
});
const completed = await client.documents.waitForGeneration(doc.id, {
interval: 2000, // poll every 2s (default)
timeout: 120000, // give up after 120s (default)
signal: AbortSignal.timeout(30000), // optional AbortSignal
});
console.log(completed.download_url);Documents and document cards share a DocumentStatus type:
import type { DocumentStatus } from 'pdfmonkey';
// 'draft' | 'pending' | 'generating' | 'success' | 'failure' | 'error'const page = await client.documentTemplates.list({ workspace_id: 'ws_xxx' });
const template = await client.documentTemplates.get('tpl_xxx');
const created = await client.documentTemplates.create({ identifier: 'invoice' });
const updated = await client.documentTemplates.update('tpl_xxx', { identifier: 'receipt' });
await client.documentTemplates.delete('tpl_xxx');Leave
pdf_engine_draft_idunset oncreate/update— the API auto-selects the latest engine. Override only when an end-user explicitly asks to pin a specific engine version (see PDF Engines below).
Read-only list of available rendering engines. Most callers do not need this. Use only when an end-user explicitly wants to pin a template to a specific engine version.
const engines = await client.pdfEngines.list();
// [{ id: 'eng_xxx', name: 'chromium', version: 6, deprecated_on: null }, ...]
await client.documentTemplates.update('tpl_xxx', {
pdf_engine_draft_id: engines[0]!.id,
});All list methods return a Page<T> with built-in navigation:
const page = await client.documentCards.list({ document_template_id: 'tpl_xxx' });
console.log(page.data); // items on this page
console.log(page.currentPage); // 1
console.log(page.totalPages); // 5
if (page.hasNextPage()) {
const next = await page.getNextPage();
}Register webhook endpoints:
const hook = await client.restHooks.create({
url: 'https://example.com/webhook',
events: ['document.done'],
});
await client.restHooks.delete(hook.id);Verify incoming webhook signatures (Svix HMAC-SHA256):
import { verifyWebhook } from 'pdfmonkey';
const event = await verifyWebhook(
rawBody,
{
'svix-id': req.headers['svix-id'],
'svix-timestamp': req.headers['svix-timestamp'],
'svix-signature': req.headers['svix-signature'],
},
process.env.WEBHOOK_SECRET,
);
// `WebhookEvent` is a discriminated union — narrow on `type`
if (event.type === 'document.done') {
console.log(event.data.download_url);
} else if (event.type === 'document.error') {
console.log(event.data.failure_cause);
}Two integrations have small but easy-to-miss requirements. Everything else is the verify call above.
Express — capture the raw body, otherwise the JSON body parser mutates it and the signature stops matching:
import express from 'express';
import { verifyWebhook } from 'pdfmonkey';
const app = express();
app.post(
'/pdfmonkey-webhook',
express.raw({ type: 'application/json' }),
async (req, res) => {
try {
const event = await verifyWebhook(
req.body.toString('utf8'),
{
'svix-id': req.header('svix-id') ?? '',
'svix-timestamp': req.header('svix-timestamp') ?? '',
'svix-signature': req.header('svix-signature') ?? '',
},
process.env.WEBHOOK_SECRET ?? '',
);
// handle event
res.status(204).end();
} catch {
res.status(400).send('Invalid signature');
}
},
);Next.js App Router — pick the runtime ('nodejs' or 'edge', both work) and read the raw body via request.text():
// app/api/pdfmonkey-webhook/route.ts
import { verifyWebhook } from 'pdfmonkey';
export const runtime = 'nodejs';
export async function POST(request: Request): Promise<Response> {
const rawBody = await request.text();
try {
const event = await verifyWebhook(
rawBody,
{
'svix-id': request.headers.get('svix-id') ?? '',
'svix-timestamp': request.headers.get('svix-timestamp') ?? '',
'svix-signature': request.headers.get('svix-signature') ?? '',
},
process.env.WEBHOOK_SECRET ?? '',
);
// handle event
return new Response(null, { status: 204 });
} catch {
return new Response('Invalid signature', { status: 400 });
}
}// Snippets
const snippets = await client.snippets.list();
await client.snippets.create({ identifier: 'header', code: '<div>Header</div>', workspace_id: 'ws_xxx' });
// Template Folders
const folders = await client.templateFolders.list();
// Workspaces
const workspaces = await client.workspaces.list();
// Current User
const user = await client.currentUser.get();const client = new PDFMonkey({
apiKey: 'your-api-key', // 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
retryDelay: (attempt) => attempt * 250, // custom backoff (optional)
hooks: { // request/response/error interceptors
onRequest: (ctx) => { ctx.headers['X-Trace-Id'] = newTraceId(); },
onResponse: (ctx) => metrics.observe(ctx.durationMs, ctx.response.status),
},
});When apiKey is omitted, the client reads process.env.PDFMONKEY_API_KEY.
Every resource method accepts a trailing options object for signal, timeout, and maxRetries:
await client.documents.create(
{ document_template_id: 'tpl_xxx', payload: { invoice: 1 } },
{
signal: AbortSignal.timeout(10_000),
timeout: 60_000,
},
);Dynamic per-call headers (trace IDs, etc.) go through the onRequest hook in ClientOptions.hooks — mutate ctx.headers there.
The SDK uses only Web Crypto + global fetch, so verifyWebhook and the client both run on Cloudflare Workers, Vercel Edge, Deno, and Bun in addition to Node 20+. Pass a custom fetch if your runtime needs a wrapped one.
import { APIConnectionError, AuthenticationError, NotFoundError, RateLimitError } from 'pdfmonkey';
try {
await client.documents.get('doc_xxx');
} catch (error) {
if (error instanceof AuthenticationError) {
// Invalid API key (401)
} else if (error instanceof NotFoundError) {
// Resource not found (404)
} else if (error instanceof RateLimitError) {
console.log(error.retryAfter); // seconds from Retry-After header
} else if (error instanceof APIConnectionError) {
console.log(error.cause); // original network error (native Error.cause)
}
}MIT