Skip to content

TypeScript and JavaScript client

@typefold/client is the npm package for the Typefold API, for TypeScript and JavaScript. It renders reports, HTML and template versions, email and SMS text, and batches, and handles render status, cancellation and downloads. The .NET, Python and TypeScript clients have the same features. For templates, assets, webhooks and administration, call the HTTP API.

  • Node.js 20.3 or later, on a server. The API sends no CORS headers, so the client does not work in a browser, and your API key stays on the server.
  • An API key with the render:create, render:read and template:read scopes. Create it in Studio under Manage → API keys; see API keys and scopes.
  • A published template to render, such as one named Invoice. See Build your first template.
Terminal window
npm install @typefold/client

It has no dependencies; it uses the built-in fetch. Use a client no newer than your server; see versioning.

import { TypefoldClient } from "@typefold/client";
const client = new TypefoldClient({
baseUrl: process.env.PDF_URL!,
apiKey: process.env.API_KEY!,
});

Set PDF_URL to your installation’s address, the URL you open Studio at, and API_KEY to your key. In JavaScript, leave out the !. Create one client per API key and reuse it for the lifetime of your application.

renderReport renders the published version of a template, found by name, and returns the file’s bytes:

import { writeFile } from "node:fs/promises";
import { TemplateRef } from "@typefold/client";
const pdf = await client.renderReport({
template: TemplateRef.byName("Invoice"),
data: { number: "2026-0042", customer: { name: "Ada Lovelace" } },
});
await writeFile("invoice.pdf", pdf);

Open invoice.pdf: you should see the template filled with your data, so {{customer.name}} prints Ada Lovelace.

For an Excel file from a spreadsheet template, set format: ReportFormat.Xlsx. To change the paper, margins or PDF standard for one render, set pdf.

render renders through the Renders API: HTML you send, or a published template version. It creates the render, waits for it and downloads the PDF.

import { PageSize } from "@typefold/client";
const pdf = await client.render({
document: {
html: "<h1>Invoice 42</h1><p>Total: 1,250.00 EUR</p>",
css: ["h1 { color: #0b5394; }"],
},
settings: { pageSize: PageSize.Letter },
});

To render a published template version, pass its ID and the data:

const ticket = await client.render({
templateVersionId: versionId,
data: { ticket: { number: "TK-1" } },
});

render waits two minutes by default; pass { timeoutMs: 300_000 } to wait five. Without settings, the page is A4 with 10 mm margins.

For long or numerous renders, create the render, store its ID, and pick up the PDF later:

import { RenderState } from "@typefold/client";
// 1. Create. Returns as soon as the render is queued.
let render = await client.createRender(request);
await saveRenderId(orderId, render.id);
// 2. Later, in a job or after a webhook arrives:
render = await client.getRender(renderId);
if (render.state === RenderState.Succeeded) {
const pdf = await client.download(render);
}

To wait in the same process instead, call client.waitForRender(render), which polls until the render finishes. To be called when it finishes, use webhooks. If you no longer need a render, call client.cancelRender(render.id).

createBatch creates up to 100 renders in one request; if any item is invalid, none is created. waitForBatch waits until every item has finished, and downloadBatch returns one PDF of every item in order:

let batch = await client.createBatch(
tickets.map((ticket) => ({ templateVersionId: versionId, data: ticket })),
{ idempotencyKey: "tickets-2026-10-12" },
);
batch = await client.waitForBatch(batch);
if (batch.counts.succeeded === batch.total) {
const pdf = await client.downloadBatch(batch.id);
}

A failed item does not stop the others. batch.items gives each item’s render id and state, to download or cancel it as a single render. See Render many documents at once.

renderReport, createRender, render and createBatch take an optional idempotencyKey. Retrying with the same key returns what the first call created instead of rendering again, so a network failure never produces a duplicate document:

const idempotencyKey = `invoice-${invoice.id}-v${invoice.revision}`;
const pdf = await client.renderReport(request, { idempotencyKey });

Use a key that names the document. For reports, keys apply to PDF only. The rules are in idempotency keys.

The client retries 429 RATE_LIMITED for you. Every other API error throws TypefoldError; its code says what went wrong and retryable whether trying again can help:

import { TypefoldError } from "@typefold/client";
try {
const pdf = await client.renderReport(request, { idempotencyKey });
} catch (error) {
if (!(error instanceof TypefoldError)) {
throw error;
}
if (error.code === "RENDER_TIMEOUT") {
// Still rendering. Follow it later with client.getRender(error.error.renderId).
await queueFollowUp(error.error.renderId!);
} else if (error.retryable) {
// RATE_LIMITED, a 5xx, and similar: back off and retry with the same idempotency key.
} else {
// Not worth retrying as is: fix the request, template or key.
console.error("PDF render failed:", error.code);
}
}

Every code is in the error reference.

renderString fills a template string and returns the text, with no PDF. See Render email and SMS text.

import { StringEngine, StringFormat } from "@typefold/client";
const body = await client.renderString({
template: "{{#if (isEqual Status 'paid')}}Paid{{else}}Due{{/if}}: {{Booking.Code}}",
data: model,
engine: StringEngine.Handlebars,
helpers: "function isEqual(a, b) { return a === b; }",
format: StringFormat.Html,
});

StringFormat.Html escapes values and StringFormat.Text (the default) does not. Without engine, only {{path}} placeholders are filled.

Everything is exported from @typefold/client, with types.

new TypefoldClient({ baseUrl, apiKey, ... }):

Option Default Meaning
baseUrl Required The API’s address, such as https://pdf.example.com. A base path such as /pdf is kept.
apiKey Required Sent as x-api-key on each API request, never to download URLs.
requestTimeoutMs 120000 How long one HTTP request may take. Keep it above the Reports API wait of 95 seconds.
maxRateLimitRetries 3 How many times a 429 is sent again, after its Retry-After (else 1, 2, 4… seconds, at most 30). 0 throws the first one.
fetch Global fetch The fetch to send requests with, such as one with a proxy agent.

Every method also takes an AbortSignal as { signal } in its last argument.

Method Returns Scopes
renderReport(request, { idempotencyKey }) Uint8Array render:create, template:read, plus render:read for PDF
renderString(request) string render:create
listReports() ReportSummary[] template:read
getReportDataSchema(format, template) JSON Schema object template:read
createRender(request, { idempotencyKey }) Render render:create
getRender(id) Render render:read
waitForRender(render, { timeoutMs }) Render render:read
download(render) Uint8Array render:read, only when it refreshes the link
render(request, { idempotencyKey, timeoutMs }) Uint8Array render:create, render:read
cancelRender(id) Nothing render:cancel
createBatch(items, { idempotencyKey }) RenderBatch render:create
getBatch(id) RenderBatch render:read
waitForBatch(batch, { timeoutMs }) RenderBatch render:read
downloadBatch(id) Uint8Array render:read

waitForRender and waitForBatch poll every 1 to 5 seconds and throw RENDER_TIMEOUT after timeoutMs: two minutes for a render, 10 minutes for a batch. A dropped connection or a retryable error such as RATE_LIMITED does not stop them. download re-reads the render first when its download link is about to expire. Aborting a signal stops your code waiting, not the render; call cancelRender for that.

Requests are plain objects with the API’s own field names, typed in the package.

Type Used for
RenderReportRequest renderReport. Its pdf field is a ReportPdfOptions.
DirectRenderRequest Your own HTML, in render, createRender and createBatch.
TemplateVersionRenderRequest A published template version, in render, createRender and createBatch.
RenderSettings The settings of a DirectRenderRequest: paper, margins, PDF standard.
RenderStringRequest renderString.

Document properties, attachments, passwords, signatures, added pages and watermarks use PdfMetadata, PdfAttachment, PdfEncryption, PdfSignature, PdfOperation and PdfWatermark. format: RenderFormat.Png, Jpeg or Webp on a render or report request, with image (ImageOptions), returns one page as an image. TemplateRef.byName, byId and byShortid pick the template; byShortid is for templates you migrated. PageSize has A3, A4, A5, Letter and Legal, or use any { widthMm, heightMm }. test: true on a render or report request makes a free, watermarked test render. What each field does is in the API reference and PDF standards and security.

data is sent as JSON.stringify writes it, with its keys unchanged, so { CustomerName: "Ada" } is {{CustomerName}} in the template, and null is kept. A Date is written in ISO 8601, and undefined values are left out. A template version or string render without data sends {}.

TypefoldError has status (undefined when the client raised the error itself), code, retryable and error, the API’s error with code, message, retryable, details, diagnostics and renderId; see the error envelope. The client raises these codes itself:

Code When
RENDER_TIMEOUT A wait ran out of time. Retryable. For a render, follow renderId.
RENDER_FAILED A render was cancelled while render waited. A failed render throws with the error it recorded instead.
HTTP_ERROR A response that was not the API’s error format, such as a proxy’s error page or a failed download. Retryable for 429 and 5xx.