Docs
Node.js SDK
Use @relaypdf/sdk in server-side Node.js or TypeScript when you want a typed client for the full public API. It maps 1:1 to REST (fromHtml, fromUrl, fromMarkdown, fromTemplate, plus images, convert, and PDF tools). Coding agents should run relaypdf setup and read RELAYPDF_API_KEY from the env file.
The official Node.js SDK is TypeScript-first, uses native fetch, and is meant for server-side Node. It supports binary, URL, and async workflows plus HMAC webhook verification. For raw HTTP, see the curl quickstart. For every REST field, see POST /v1/pdf and rendering options.
Installation
Requires a current Node.js LTS with global fetch (Node 18+).
Package: @relaypdf/sdk on npm. Current published line is 0.1.x. Zero runtime dependencies. The npm README is the full method, error, and type reference.
npm install @relaypdf/sdkQuick start
Create a client with your API key. Coding agents should load RELAYPDF_API_KEY from the env file written by relaypdf setup.
import { RelayPDF } from "@relaypdf/sdk";
const client = new RelayPDF({
apiKey: process.env.RELAYPDF_API_KEY!,
});
const pdf = await client.pdf.fromHtml(
"<h1>Invoice #1042</h1><p>Total: $1,200.00</p>",
{ filename: "invoice.pdf" },
);
await pdf.save("invoice.pdf");
console.log(pdf.id, pdf.sizeBytes, pdf.contentType);Convert a URL to PDF
Use fromUrl when RelayPDF should fetch a public HTTPS page and render it with Chromium. Private, loopback, and metadata hosts are rejected (url_not_allowed).
const pdf = await client.pdf.fromUrl("https://example.com", {
filename: "page.pdf",
options: {
format: "A4",
printBackground: true,
waitUntil: "networkidle0",
},
});
await pdf.save("page.pdf");Convert HTML to PDF
fromHtml sends the HTML string as JSON (UTF-8, not Base64). printBackground defaults to true on the API.
const pdf = await client.pdf.fromHtml("<h1>Hello from RelayPDF</h1>", {
options: { format: "letter" },
});
await pdf.save("hello.pdf");Headers and footers
Pass Chromium header/footer HTML on options. Use extra top/bottom margin so they are not clipped. Placeholders follow Chromium: pageNumber, totalPages, date, title.
Set headerTemplate or footerTemplate on options. The REST field names are the same as in the SDK options object.
const pdf = await client.pdf.fromHtml("<h1>Invoice</h1>", {
options: {
headerTemplate:
'<div style="font-size:9px;width:100%;text-align:center;">Invoice</div>',
footerTemplate:
'<div style="font-size:9px;width:100%;text-align:center;">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>',
margin: { top: "20mm", bottom: "20mm" },
},
});Markdown and templates
fromMarkdown renders GitHub-flavoured Markdown through Chromium. fromTemplate uses a published Handlebars layout (UUID or slug) plus JSON templateData.
const md = await client.pdf.fromMarkdown("# Hello\n\nFrom **Markdown**.");
const invoice = await client.pdf.fromTemplate(
"invoice",
{ number: "INV-1042", total: 1458 },
{ filename: "invoice.pdf", strict: true },
);Binary results
Default response is binary. The result kind is "binary". bytes is a Uint8Array. save(path) writes the file in Node.
- • id comes from the x-relaypdf-id response header
- • sizeBytes comes from x-relaypdf-size when present
- • filename is parsed from content-disposition
const pdf = await client.pdf.fromUrl("https://example.com");
if (pdf.kind === "binary") {
console.log(pdf.id);
console.log(pdf.filename);
console.log(pdf.sizeBytes);
console.log(pdf.contentType);
console.log(pdf.bytes.byteLength);
await pdf.save("example.pdf");
}Temporary URL
Set response: "url" when you want JSON with a public download that expires in 24 hours. GET /v1/files/:id does not need the API key.
const result = await client.pdf.fromHtml("<h1>Invoice</h1>", {
response: "url",
filename: "invoice.pdf",
});
if (result.kind === "url") {
console.log(result.url);
console.log(result.expiresAt);
console.log(result.sizeBytes);
}Async jobs
Set response: "async" for a 202. Poll with jobs.wait (or jobs.get). Optional callbackUrl receives the job payload when it finishes.
const job = await client.convert.fromPath("./deck.pptx", {
to: "pdf",
response: "async",
});
if (job.kind === "async") {
const done = await client.jobs.wait(job.id, {
intervalMs: 1500,
timeoutMs: 120_000,
});
const file = await client.files.download(done.id);
await file.save("deck.pdf");
}Images, convert, and tools
The same client covers the rest of the locked public API. file accepts Uint8Array, Buffer, ArrayBuffer, or an existing base64 string.
const shot = await client.images.fromUrl("https://example.com", {
options: { fullPage: true, type: "png" },
});
const fromWord = await client.convert.fromPath("./letter.docx", { to: "pdf" });
const docx = await client.convert.fromHtml("<h1>Report</h1>", { to: "docx" });
const pack = await client.pdf.merge({
files: [
{ url: "https://example.com/cover.pdf" },
{ file: fromWord.kind === "binary" ? fromWord.bytes : new Uint8Array() },
],
});
const qr = await client.barcodes.qr("https://relaypdf.com");Account and health
health() does not send a key. account() returns plan, rate tier, and wallet millicents and is not billed.
const live = await client.health();
console.log(live.ok, live.service);
const account = await client.account();
console.log(account.plan, account.rateTier, account.wallet.balanceMillicents);Verify webhook signatures
Use the exact raw body string from your framework. Do not JSON.parse and re-stringify before verifyWebhook. The secret is the dashboard webhook secret, not the API key.
The header format is t=<unix>,v1=<hex hmac of timestamp.body>. Default clock skew tolerance is 300 seconds.
import { verifyWebhook, WEBHOOK_SIGNATURE_HEADER } from "@relaypdf/sdk";
const ok = await verifyWebhook(
process.env.RELAYPDF_WEBHOOK_SECRET!,
rawBody,
request.headers.get(WEBHOOK_SIGNATURE_HEADER) ?? "",
);
if (!ok) return new Response("invalid signature", { status: 400 });Error handling
API errors throw RelayPDFError. Failed operations are never billed. Check status for HTTP and code for the RelayPDF error code.
import { RelayPDF, RelayPDFError } from "@relaypdf/sdk";
try {
await client.pdf.fromUrl("https://example.com");
} catch (error) {
if (error instanceof RelayPDFError) {
console.log(error.status);
console.log(error.code);
console.log(error.message);
console.log(error.retryAfter);
if (error.code === "payment_required") {
// top up the wallet
}
if (error.code === "rate_limited") {
console.log("Retry-After", error.retryAfter);
}
} else {
throw error;
}
}Client options
apiKey is required. Missing apiKey throws TypeError before any request. Optional fetch lets you inject an HTTP implementation (tests, undici, Cloudflare).
The SDK does not automatically retry. One method call is one HTTP request. Conversion timeout (options.timeout, max 60s) is a Chromium render limit, not an SDK HTTP timeout.
const client = new RelayPDF({
apiKey: process.env.RELAYPDF_API_KEY!,
baseUrl: "http://localhost:8787",
fetch: globalThis.fetch,
});SDK reference
Every generating method accepts response: "binary" | "url" | "async". URIs are relative to https://api.relaypdf.com. The npm README lists the same table.
Also exported: RelayPDFError, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_EVENT_HEADER, DEFAULT_BASE_URL. Types: BinaryResult, UrlResult, AsyncResult, Job, PdfOptions, ImageOptions, FileRef.
| Method | HTTP | Description |
|---|---|---|
| health() | GET /health | Liveness; no API key |
| account() | GET /v1/account | Plan, rate tier, millicents; not billed |
| pdf.fromHtml / fromUrl / fromMarkdown / fromTemplate / create | POST /v1/pdf | HTML, URL, Markdown, or published template → PDF |
| pdf.merge | POST /v1/pdf/merge | Merge 2–20 PDFs |
| pdf.extract | POST /v1/pdf/extract | Extract / split page ranges |
| pdf.protect / unlock | POST /v1/pdf/protect · /unlock | Password-protect or remove password |
| pdf.bookmarks | POST /v1/pdf/bookmarks | Outline bookmarks |
| pdf.raster | POST /v1/pdf/raster | Pages → png/jpeg (zip if many) |
| pdf.fromImages | POST /v1/pdf/from-images | PNG/JPEG → PDF |
| pdf.stamp | POST /v1/pdf/stamp | Text or image watermark |
| pdf.rotate / deletePages / compress | POST /v1/pdf/rotate · /delete-pages · /compress | Rotate, delete pages, lossless optimize |
| pdf.info / text | POST /v1/pdf/info · /text | Metadata JSON; existing text layer |
| pdf.formFields / formFill | POST /v1/pdf/form/fields · /form/fill | List or fill AcroForm fields |
| images.fromHtml / fromUrl / create | POST /v1/images | HTML or URL → png/jpeg/webp |
| convert.fromHtml / fromPath / wkhtml / create | POST /v1/convert | LibreOffice or wkhtmltopdf |
| templates.list / gallery / get / create / update / delete | GET/POST/PATCH/DELETE /v1/templates | Handlebars drafts and stock gallery |
| templates.publish / discard / duplicate / versions / restore | POST/GET /v1/templates/:id/… | Publish, discard, duplicate, versions |
| templates.validate / preview / generate | POST /v1/templates/validate · /preview · /generate | Validate, Chromium preview, AI create/edit |
| barcodes.create / qr | POST /v1/barcodes | Barcode / QR image |
| zip.create | POST /v1/zip | Zip named files |
| jobs.get / wait | GET /v1/jobs/:id | Poll; wait until completed or failed |
| files.download | GET /v1/files/:id | 24h download; unauthenticated |
| webhooks.list / create / delete | GET/POST/DELETE /v1/webhooks | Signed endpoint CRUD; secret on create only |
| verifyWebhook | RelayPDF-Signature | HMAC-SHA256 of {t}.{raw_body} |
Errors
API errors throw RelayPDFError. Branch on error.code. Failed operations are never billed.
| code | HTTP | Meaning |
|---|---|---|
| invalid_request | 400 | Bad fields or exclusive sources |
| url_not_allowed | 400 | Private URL or non-https callbackUrl |
| unauthorized | 401 | Missing or unknown API key |
| payment_required | 402 | Empty wallet |
| account_suspended | 403 | Keys cannot be used |
| not_found | 404 | Unknown job, file, or template |
| payload_too_large | 413 | HTML or file too large |
| rate_limited | 429 | Honor retryAfter; unbilled |
| render_failed / processing_failed | 502 | Unbilled |
| convert_unavailable | 503 | Document worker cold or down; unbilled |
| internal_error | 500 | Retry; unbilled |