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
npm install @relaypdf/sdk

Quick start

Create a client with your API key. Coding agents should load RELAYPDF_API_KEY from the env file written by relaypdf setup.

index.ts
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).

fromUrl
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.

fromHtml
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.

headerTemplate / footerTemplate
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.

fromMarkdown / fromTemplate
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
BinaryResult
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.

response: url
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.

async + wait
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.

images / convert / merge
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.

health / account
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.

verifyWebhook
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.

RelayPDFError
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.

RelayPDFOptions
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.

MethodHTTPDescription
health()GET /healthLiveness; no API key
account()GET /v1/accountPlan, rate tier, millicents; not billed
pdf.fromHtml / fromUrl / fromMarkdown / fromTemplate / createPOST /v1/pdfHTML, URL, Markdown, or published template → PDF
pdf.mergePOST /v1/pdf/mergeMerge 2–20 PDFs
pdf.extractPOST /v1/pdf/extractExtract / split page ranges
pdf.protect / unlockPOST /v1/pdf/protect · /unlockPassword-protect or remove password
pdf.bookmarksPOST /v1/pdf/bookmarksOutline bookmarks
pdf.rasterPOST /v1/pdf/rasterPages → png/jpeg (zip if many)
pdf.fromImagesPOST /v1/pdf/from-imagesPNG/JPEG → PDF
pdf.stampPOST /v1/pdf/stampText or image watermark
pdf.rotate / deletePages / compressPOST /v1/pdf/rotate · /delete-pages · /compressRotate, delete pages, lossless optimize
pdf.info / textPOST /v1/pdf/info · /textMetadata JSON; existing text layer
pdf.formFields / formFillPOST /v1/pdf/form/fields · /form/fillList or fill AcroForm fields
images.fromHtml / fromUrl / createPOST /v1/imagesHTML or URL → png/jpeg/webp
convert.fromHtml / fromPath / wkhtml / createPOST /v1/convertLibreOffice or wkhtmltopdf
templates.list / gallery / get / create / update / deleteGET/POST/PATCH/DELETE /v1/templatesHandlebars drafts and stock gallery
templates.publish / discard / duplicate / versions / restorePOST/GET /v1/templates/:id/…Publish, discard, duplicate, versions
templates.validate / preview / generatePOST /v1/templates/validate · /preview · /generateValidate, Chromium preview, AI create/edit
barcodes.create / qrPOST /v1/barcodesBarcode / QR image
zip.createPOST /v1/zipZip named files
jobs.get / waitGET /v1/jobs/:idPoll; wait until completed or failed
files.downloadGET /v1/files/:id24h download; unauthenticated
webhooks.list / create / deleteGET/POST/DELETE /v1/webhooksSigned endpoint CRUD; secret on create only
verifyWebhookRelayPDF-SignatureHMAC-SHA256 of {t}.{raw_body}

Errors

API errors throw RelayPDFError. Branch on error.code. Failed operations are never billed.

codeHTTPMeaning
invalid_request400Bad fields or exclusive sources
url_not_allowed400Private URL or non-https callbackUrl
unauthorized401Missing or unknown API key
payment_required402Empty wallet
account_suspended403Keys cannot be used
not_found404Unknown job, file, or template
payload_too_large413HTML or file too large
rate_limited429Honor retryAfter; unbilled
render_failed / processing_failed502Unbilled
convert_unavailable503Document worker cold or down; unbilled
internal_error500Retry; unbilled