Renduo
Guides

Node.js SDK (@renduo/sdk)

Generate PDFs from TypeScript or Node.js with the official SDK — typed, zero runtime dependencies, and no base64 to decode by hand.

If your backend runs on Node.js, the official SDK is the fastest way to call Renduo: it is a thin, typed client over native fetch with zero runtime dependencies, and it returns the PDF already decoded as a Buffer — no manual base64 handling, no hand-rolled error parsing.

Not required. The SDK is a convenience layer over the same HTTP API the quickstart calls with curl. If you prefer raw requests (Node, Python, Go, …) or you only need a one-off script, skip it — see the backend examples.

Install

npm install @renduo/sdk

Requires Node ≥ 20 (native fetch). ESM only — see Module format below.

Quickstart

import { Renduo } from '@renduo/sdk'
import { writeFile } from 'node:fs/promises'

const renduo = new Renduo({ apiKey: process.env.RENDUO_API_KEY! })

const { pdf } = await renduo.generate({
  templateId: '5f0c2e3a-…', // Dashboard → Templates, or renduo templates list
  payload: {
    title: 'Invoice 2026-001',
    customer: { name: 'Acme Corp' },
    items: [
      { description: 'Starter Plan', amount: 19000 },
      { description: 'Tax (19%)', amount: 3610 },
    ],
    total: 22610,
  },
})

await writeFile('invoice.pdf', pdf)

pdf is a Buffer you can write to disk, stream into an HTTP response, or attach to an email — no base64 step. sync is the default mode: the call waits for the PDF and returns once it's ready.

Async generation

For high volume or background jobs, use mode: 'async' and either poll with waitForGeneration or listen for the generation.completed webhook registered in Dashboard → Settings → Webhooks.

const job = await renduo.generate({ templateId, payload, mode: 'async' })
// job: { jobId, status: 'queued' }

// Polls GET /v1/generate/:jobId until ready, then downloads the PDF.
const result = await renduo.waitForGeneration(job.jobId)
// result.pdf is a Buffer — same as sync mode.

waitForGeneration polls every 1.5s with a 60s client-side timeout (both configurable via { pollIntervalMs, timeoutMs }). It throws RenduoGenerationError if the job ends in failed or timeout, with the job's errorMessage and status attached:

import { RenduoGenerationError } from '@renduo/sdk'

try {
  const result = await renduo.waitForGeneration(job.jobId)
} catch (err) {
  if (err instanceof RenduoGenerationError) {
    console.error(`${err.status}: ${err.message}`) // e.g. "failed: …"
  }
}

For an event-driven pipeline instead of polling, register a webhook endpoint that subscribes to generation.completed / generation.failed and verify the incoming signature with the SDK (below).

Templates

const { templates, total } = await renduo.templates.list()

const template = await renduo.templates.get(templates[0].id)
// template.versions: [{ version, status: 'active' | 'disabled', createdAt }, …], newest first

templates.get(id) throws RenduoApiError with code: 'TEMPLATE_NOT_FOUND' if the template doesn't exist.

Rolling back a version

templates.activate(id, version) makes a previously published version the active one again, disabling the current one in a single transaction:

const { activeVersion, previousActiveVersion } = await renduo.templates.activate(templateId, 2)
// → activeVersion: 2, previousActiveVersion: 3

It is idempotent — activating the version that is already active resolves without changing anything, so a retried deploy step is safe. It throws RenduoApiError with code: 'VERSION_NOT_ACTIVATABLE' (409) if that version never finished publishing, and 'TEMPLATE_NOT_FOUND' (404) if the template or version doesn't exist. Requires a key with the templates:write scope.

See Templates & versions for what a rollback does and does not change.

Verifying webhooks

import { verifyWebhookSignature } from '@renduo/sdk'

// Inside your webhook route handler:
const signature = request.headers.get('renduo-signature')
const rawBody = await request.text() // raw body — never a re-serialized JSON.parse/stringify round trip

if (!verifyWebhookSignature(process.env.RENDUO_WEBHOOK_SECRET!, rawBody, signature!)) {
  return new Response('Invalid signature', { status: 401 })
}

const event = JSON.parse(rawBody) // { event: 'generation.completed', generationId, pdfUrl, … }

The function returns false — never throws — for a malformed header, a signature mismatch, or a timestamp older than 5 minutes (replay protection).

Error handling

Every non-2xx response throws RenduoApiError with the API's typed error code, message, and request id:

import { RenduoApiError } from '@renduo/sdk'

try {
  await renduo.generate({ templateId, payload })
} catch (err) {
  if (err instanceof RenduoApiError && err.code === 'QUOTA_EXCEEDED') {
    // Free plan limit reached — paid plans accumulate overage instead.
  }
  throw err
}

See the errors reference for the full list of codes.

Module format

ESM only. A CommonJS require('@renduo/sdk') won't work — run your project as ESM ("type": "module" in package.json, or .mjs) or use dynamic import('@renduo/sdk') from CommonJS code.

See also

On this page