@dynamic-document-api/sdk
v0.2.0
Published
Official Dynamic Document API SDK — turn templates and JSON into PDFs and images.
Maintainers
Readme
Dynamic Document API for TypeScript and JavaScript
The official SDK for Dynamic Document API: turn templates and JSON into PDFs, images and e-invoices, from EU data centres.
npm install @dynamic-document-api/sdkIt runs on Node.js 18 and later, Deno, Bun and edge runtimes. It has no dependencies: it uses fetch,
AbortController and Web Crypto. Keep your API key on the server; don't ship it to browsers.
Quickstart
import { DynamicDocumentApi } from "@dynamic-document-api/sdk";
const dda = new DynamicDocumentApi(); // reads DYNAMIC_DOCUMENT_API_KEY
const render = await dda.pdf.fromTemplate({
templateId: "tpl_…",
data: { number: "2026-0042", customer: { name: "Ada Lovelace" } },
});
console.log(render.files?.[0]?.url); // a signed download linkCreate a key in the dashboard under API keys. A test key (dda_test_…) renders for free, with a watermark; a live
key (dda_live_…) counts against your plan.
Renders
// From HTML, a URL or Markdown
const fromHtml = await dda.pdf.fromHtml({ html: "<h1>Hello</h1>", pdf: { paper: { size: "A4" } } });
const screenshot = await dda.images.fromUrl({ url: "https://example.com", format: "png" });
// The unified endpoint takes every input type and output format
const render = await dda.renders.create({
input: { type: "template", templateId: "tpl_…" },
data: { number: 42 },
output: { format: "pdf" },
mode: "async", // answer at once and render in the background
});
const done = await dda.waitForRender(render.id); // polls until it is finished
// The file in the response body instead of a link
const binary = await dda.renders.createBinary({ input: { type: "html", html: "<h1>Hi</h1>" } });
const bytes = await binary.bytes();
// Download a hosted file
const pdf = await dda.files.download(done.files![0]!);Batches
const batch = await dda.batches.create({
templateId: "tpl_…",
items: invoices.map((invoice) => ({ data: invoice, reference: invoice.number })),
combine: { zip: true },
});
const finished = await dda.waitForBatch(batch.id);
for await (const item of dda.batches.listItems(batch.id, { status: "failed" })) console.log(item.index, item.error);PDF tools and e-invoices
const merged = await dda.pdfTools.merge({ sources: [{ renderId: a.id }, { renderId: b.id }] });
const archived = await dda.pdfTools.pdfa({ source: { renderId: render.id }, level: "2b" });
// Standalone XRechnung or Factur-X XML
const xml = await dda.einvoices.create({ profile: "XRECHNUNG", invoice: { /* EN 16931 fields */ } });Files for the PDF tools, attachments and batch CSVs go in with dda.uploads.create({ file, filename }); uploads
are kept for 24 hours.
Templates
for await (const template of dda.templates.list()) console.log(template.id, template.name);
const created = await dda.templates.create({ name: "Invoice", type: "pdf_code", html: "<h1>Invoice {{ number }}</h1>" });
await dda.templates.updateDraft(created.id, { html: "<h1>Invoice {{ number }}</h1><p>{{ customer.name }}</p>" });
await dda.templates.publish(created.id, { note: "First version" });Email delivery and storage
Send rendered documents through your own email provider, or upload them to your own bucket or SFTP server.
const connection = await dda.emailConnections.create({
name: "Billing",
provider: "brevo",
defaultFrom: { email: "[email protected]", name: "Example Ltd" },
credentials: { api_key: process.env.BREVO_API_KEY },
});
await dda.emailRules.create("tpl_…", {
name: "Send the invoice",
connectionId: connection.id,
to: "{{ customer.email }}",
subject: "Invoice {{ number }}",
});
for await (const send of dda.emailSends.list({ status: "failed" })) console.log(send.id);
await dda.storageDestinations.create({
name: "Archive",
provider: "s3",
config: { bucket: "acme-invoices", region: "eu-central-1" },
credentials: { access_key_id: "…", secret_access_key: "…" },
});Signed links
A signed link renders an image or PDF from a template on request, for <img> tags, emails and social cards.
sign() builds the URL locally with the link's secret, without an API call:
const link = await dda.signedLinks.create({
templateId: "tpl_…",
name: "Social card",
formats: ["png"],
allowedParams: ["title.text"],
});
// link.secret is shown once; store it
const url = await dda.signedLinks.sign(link.id, process.env.DDA_LINK_SECRET!, {
params: { "title.text": "Summer Sale" },
expiresIn: 3600,
});Webhooks
await dda.webhookEndpoints.create({ url: "https://example.com/hooks/dda", events: ["render.succeeded", "batch.completed"] });
// In your handler: verify with the raw body, before parsing it
const event = await dda.webhooks.verify(rawBody, request.headers, process.env.DDA_WEBHOOK_SECRET!);
if (event.type === "render.succeeded") console.log(event.data.object.id);Pass an array of secrets to verify() while you roll one with dda.webhookEndpoints.rollSecret().
Configuration
const dda = new DynamicDocumentApi({
apiKey: "dda_live_…", // default: DYNAMIC_DOCUMENT_API_KEY
region: "eu", // the EU (Germany) is the only region today
timeoutMs: 130_000, // per request; above the longest synchronous wait (120 s)
retry: { maxAttempts: 3 }, // or false
autoIdempotencyKey: true, // every POST gets an Idempotency-Key
telemetry: true, // the SDK's name and version in User-Agent
fetch: customFetch, // default: the global fetch
});
// Per call: your own idempotency key, request id, timeout, retries, headers or abort signal
await dda.pdf.fromHtml({ html }, { idempotencyKey: `invoice-${id}`, timeoutMs: 120_000 });Retries cover network errors, 429 and 5xx, with exponential backoff and jitter, and respect Retry-After.
A retried POST reuses its idempotency key, so a render is never billed twice. Request bodies can be written in
camelCase (templateId) or as in the API reference (template_id). Values you own are sent exactly as written:
data, metadata, HTTP headers, signed-link params, and the config and credentials of connections and
destinations, which use the API's own spelling.
Lists
Every list is an async iterable that fetches pages as you go:
for await (const render of dda.renders.list({ status: "failed" })) console.log(render.id);
const page = await dda.renders.list({ limit: 20 }).page();
const recent = await dda.renders.list().all(200); // pass a limit: a workspace can hold millionsErrors
Every failure is a DynamicDocumentApiError with status, code, message and requestId, and a subclass per
kind of failure:
import { RateLimitError, RenderFailedError } from "@dynamic-document-api/sdk";
try {
await dda.pdf.fromTemplate({ templateId, data });
} catch (error) {
if (error instanceof RenderFailedError) console.error(error.code, error.message); // e.g. template_runtime_error
else if (error instanceof RateLimitError) console.error("slow down");
else throw error;
}Every error code is listed at https://dynamicdocumentapi.com/docs/errors.
Anything else
dda.request() calls any endpoint with the same authentication, retries and error handling:
const account = await dda.request("GET", "/account");The full API is described at https://dynamicdocumentapi.com/docs/api-reference.
Versioning
The SDK follows semantic versioning. While it is 0.x, a minor version can change the API; see the changelog.
Licence
MIT. See LICENSE.
