npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@relaypdf/sdk

v0.1.4

Published

Official Node.js / TypeScript SDK for RelayPDF — HTML, Markdown, URL, and templates to PDF, LibreOffice convert, PDF tools, barcodes, zip, async jobs, and webhook verification

Downloads

779

Readme

@relaypdf/sdk

Official Node.js / TypeScript client for RelayPDF.

HTML to PDFs without the struggle. HTML to PDF API that converts HTML, Markdown, URLs, and Office files to production PDFs. One API for developers and coding agents.

This package covers the full public API: Chromium PDF and screenshots, Handlebars templates, LibreOffice / wkhtmltopdf convert, PDF tools, barcodes, zip, async jobs, account, signed webhooks, and webhook verification. Zero runtime dependencies. Uses native fetch.

Introduction

RelayPDF is a commercial document API. You send HTML, Markdown, a public URL, Office bytes, or a published Handlebars template. You get a production PDF (or PNG/JPEG/WebP, Word, zip, …) over REST, this SDK, the CLI, or MCP.

The JSON field names in this SDK match REST exactly (html, printBackground, sourceFilename, callbackUrl). Method names are camelCase.

Failed operations are never billed.

Base URL

Default: https://api.relaypdf.com

| Example | Path | |---------|------| | Health | https://api.relaypdf.com/health | | HTML → PDF | https://api.relaypdf.com/v1/pdf | | Convert | https://api.relaypdf.com/v1/convert | | Account | https://api.relaypdf.com/v1/account |

Override with baseUrl for local (http://localhost:8787) or a custom origin.

Definitions

| Term | Meaning | |------|---------| | API key | Secret created in Dashboard → API keys. Prefix pdf_live_… (production) or pdf_test_…. Sent as Authorization: Bearer <key>. Shown once at creation. | | Wallet | Prepaid USD balance. Ledger unit is millicents: 1 millicent = $0.001. $5.00 trial = 5000 millicents. $0.05 AI generate = 50 millicents. GET /v1/account returns wallet.balanceMillicents. | | Template | Handlebars HTML draft. Publish a version, then render with templateId (UUID or slug) + templateData. Stock layouts: pdf-templates. | | Job | Async generation (response: "async"). Poll GET /v1/jobs/:id or client.jobs.wait. | | File | 24-hour download at GET /v1/files/:id. No API key required to GET. | | Document worker | LibreOffice / wkhtml / pdftoppm container behind POST /v1/convert and POST /v1/pdf/raster. Cold start can take up to about a minute. |

Rate limiting

Trial wallets: 20 requests/minute. Funded or auto-reload: 60/minute. Burst: 5 / 10 seconds.

A 429 includes Retry-After (seconds). Rate-limited requests are not billed.

| Header | Description | |--------|-------------| | Retry-After | Seconds to wait | | x-relaypdf-id | Document or request id on success | | x-relaypdf-size | Byte size on binary responses |

Libraries and SDKs

| Surface | Package | Install | |---------|---------|---------| | Node.js / TypeScript | @relaypdf/sdk | npm install @relaypdf/sdk | | Python | relaypdf | pip install relaypdf | | PHP | relaypdf/relaypdf | composer require relaypdf/relaypdf | | C# / .NET | RelayPDF | dotnet add package RelayPDF | | Java | com.relaypdf:relaypdf | Maven com.relaypdf:relaypdf | | CLI + MCP | @relaypdf/cli | npx @relaypdf/cli setup | | n8n | n8n-nodes-relaypdf | Community Nodes → n8n-nodes-relaypdf | | REST | — | https://api.relaypdf.com | | OpenAPI | — | openapi.json |

Do not ask a human to paste an API key. Run npx @relaypdf/cli setup and approve the browser prompt.

Authentication

Every generating call except GET /health and GET /v1/files/:id requires a Bearer API key.

  1. Sign in at relaypdf.com.
  2. Open Dashboard → API keys and create a key, or run npx @relaypdf/cli setup and click Approve.
  3. Pass the key to the client. The SDK never logs it.
import { RelayPDF } from "@relaypdf/sdk";

const client = new RelayPDF({
  apiKey: process.env.RELAYPDF_API_KEY!,
  // baseUrl: "http://localhost:8787",
});

Missing apiKey throws TypeError before any request. Invalid keys return HTTP 401 unauthorized.

Error codes

Failures return { "error": { "code", "message" } }. The SDK throws RelayPDFError.

| HTTP | code | Description | |------|--------|-------------| | 400 | invalid_request | Bad fields, exclusive sources, or invalid page ranges | | 400 | url_not_allowed | Private / loopback URL, or callbackUrl is not https | | 401 | unauthorized | Missing or unknown API key | | 402 | payment_required | Wallet empty — top up or enable auto-reload | | 403 | account_suspended | Account cannot use keys | | 404 | not_found | Unknown job, file, or template | | 413 | payload_too_large | HTML or file exceeds the size limit | | 429 | rate_limited | Honor retryAfter; not billed | | 502 | render_failed | Chromium could not print; not billed | | 502 | processing_failed | PDF tool or job failed; not billed | | 503 | convert_unavailable | Document worker cold or unreachable; not billed | | 503 | ai_unavailable | Template generate backend unavailable; not billed | | 503 | storage_unavailable | Temporary file storage unavailable; not billed | | 500 | internal_error | Retry with backoff; not billed |

import { RelayPDF, RelayPDFError } from "@relaypdf/sdk";

try {
  await client.pdf.fromUrl("https://example.com");
} catch (error) {
  if (error instanceof RelayPDFError) {
    console.log(error.status, error.code, error.message, error.retryAfter);
  } else {
    throw error;
  }
}

Billing

Launch rates (admin can override; see /pricing):

| Job | Typical source | Cost | |-----|----------------|------| | HTML / URL / Markdown / template PDF | html, url, markdown, template | $0.015 | | Screenshot | image | $0.015 | | AI template generate | template_ai | $0.05 | | LibreOffice convert | convert | $0.04 | | wkhtmltopdf | wkhtml | $0.025 | | Tools (merge, stamp, raster, barcode, zip, …) | matching endpoint | $0.005 |

New accounts start with $5.00 trial (5000 millicents). Only HTTP 200 (or a completed async job) debits the wallet.

Installation

Requires Node.js 18+ (global fetch). TypeScript types ship in the package.

npm install @relaypdf/sdk
pnpm add @relaypdf/sdk
yarn add @relaypdf/sdk

From git (monorepo path packages/sdk):

npm install github:timspell1/PDF#main

The published tarball is ESM only ("type": "module").

Getting started

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);

User-Agent sent: relaypdf-node/0.1.4.

Response modes

Every generating method accepts response: "binary" | "url" | "async".

| Mode | Result | Use when | |------|--------|----------| | binary (default) | kind: "binary"bytes (Uint8Array) and save(path) | You want the file now | | url | kind: "url" — public url, expiresAt (~24h) | Share a download link | | async | kind: "async"id, pollUrl (HTTP 202) | Long convert; poll jobs.wait |

Optional callbackUrl (https only) is POSTed when that job finishes.

const link = await client.pdf.fromHtml("<h1>Hi</h1>", { response: "url" });
if (link.kind === "url") console.log(link.url, link.expiresAt);

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");
}

GET /v1/files/:id does not send the API key (files.download sets auth: false).

Client options

const client = new RelayPDF({
  apiKey: process.env.RELAYPDF_API_KEY!,
  baseUrl: "https://api.relaypdf.com",
  fetch: globalThis.fetch, // inject for tests / undici / Workers
});

The SDK does not retry. One method call is one HTTP request. options.timeout on PDF/image jobs is the Chromium render budget (max 60s), not an SDK HTTP timeout.

file accepts Uint8Array, Buffer, ArrayBuffer, or an existing base64 string. The SDK base64-encodes bytes before POST.

Documentation for API methods

All URIs are relative to https://api.relaypdf.com.

| Resource | Method | HTTP | Description | |----------|--------|------|-------------| | RelayPDF | health() | GET /health | Liveness. No API key. | | RelayPDF | account() | GET /v1/account | Plan, rate tier, wallet millicents. Not billed. | | pdf | create(input) | POST /v1/pdf | HTML, URL, Markdown, or template → PDF | | pdf | fromHtml(html, extra?) | POST /v1/pdf | HTML string → PDF | | pdf | fromUrl(url, extra?) | POST /v1/pdf | Public HTTPS page → PDF | | pdf | fromMarkdown(markdown, extra?) | POST /v1/pdf | GitHub-flavoured Markdown → PDF | | pdf | fromTemplate(templateId, templateData, extra?) | POST /v1/pdf | Published Handlebars + JSON → PDF | | pdf | merge({ files }) | POST /v1/pdf/merge | Merge 2–20 PDFs | | pdf | extract({ pages, url\|file }) | POST /v1/pdf/extract | Extract / split page ranges | | pdf | protect({ userPassword, url\|file }) | POST /v1/pdf/protect | Password-protect | | pdf | unlock({ password, url\|file }) | POST /v1/pdf/unlock | Remove password | | pdf | bookmarks({ bookmarks, url\|file }) | POST /v1/pdf/bookmarks | Add outline bookmarks | | pdf | raster({ url\|file, pages?, type?, dpi? }) | POST /v1/pdf/raster | Pages → PNG/JPEG (zip if many) | | pdf | fromImages({ files }) | POST /v1/pdf/from-images | PNG/JPEG images → PDF | | pdf | stamp({ text\|image, url\|file }) | POST /v1/pdf/stamp | Text or image watermark | | pdf | rotate({ degrees, url\|file }) | POST /v1/pdf/rotate | Rotate pages (multiples of 90) | | pdf | deletePages({ pages, url\|file }) | POST /v1/pdf/delete-pages | Delete pages | | pdf | compress({ url\|file }) | POST /v1/pdf/compress | Lossless optimize | | pdf | info({ url\|file }) | POST /v1/pdf/info | Page count and metadata JSON | | pdf | text({ url\|file }) | POST /v1/pdf/text | Extract existing text layer | | pdf | formFields({ url\|file }) | POST /v1/pdf/form/fields | List AcroForm fields | | pdf | formFill({ fields, url\|file }) | POST /v1/pdf/form/fill | Fill and flatten fields | | images | create(input) | POST /v1/images | HTML or URL → png/jpeg/webp | | images | fromHtml(html, extra?) | POST /v1/images | HTML → screenshot | | images | fromUrl(url, extra?) | POST /v1/images | URL → screenshot | | convert | create(input) | POST /v1/convert | LibreOffice or wkhtmltopdf | | convert | fromHtml(html, extra?) | POST /v1/convert | HTML → docx/xlsx/pdf/… | | convert | fromPath(path, extra?) | POST /v1/convert | Local Office file → target | | convert | wkhtml(input) | POST /v1/convert | wkhtmltopdf compatibility engine | | templates | list() | GET /v1/templates | List drafts | | templates | gallery() | GET /v1/templates/gallery | Stock Handlebars layouts | | templates | get(id) | GET /v1/templates/:id | Read template | | templates | create(input) | POST /v1/templates | Create Handlebars draft | | templates | update(id, input) | PATCH /v1/templates/:id | Update draft | | templates | delete(id) | DELETE /v1/templates/:id | Delete | | templates | publish(id, comment?) | POST /v1/templates/:id/publish | Publish active draft | | templates | discard(id) | POST /v1/templates/:id/discard | Discard draft | | templates | duplicate(id) | POST /v1/templates/:id/duplicate | Duplicate | | templates | versions(id) | GET /v1/templates/:id/versions | List versions | | templates | restore(id, version) | POST /v1/templates/:id/restore | Restore version | | templates | validate(input) | POST /v1/templates/validate | Validate Handlebars | | templates | preview(input) | POST /v1/templates/preview | Chromium preview of unsaved HTML | | templates | generate(input) | POST /v1/templates/generate | AI create/edit | | barcodes | create({ type, text }) | POST /v1/barcodes | Barcode / QR image | | barcodes | qr(text, extra?) | POST /v1/barcodes | QR helper (type: "qr") | | zip | create({ files }) | POST /v1/zip | Zip named files | | jobs | get(id) | GET /v1/jobs/:id | Poll an async job | | jobs | wait(id, { intervalMs, timeoutMs }) | GET /v1/jobs/:id | Poll until completed or failed | | files | download(id) | GET /v1/files/:id | 24h download (unauthenticated) | | webhooks | list() | GET /v1/webhooks | List signed endpoints (secret never listed) | | webhooks | create({ url, events? }) | POST /v1/webhooks | Create HTTPS endpoint; secret once; cap 25; not billed | | webhooks | delete(id) | DELETE /v1/webhooks/:id | Unsubscribe | | — | verifyWebhook(secret, body, header) | Dashboard webhook | HMAC-SHA256 of {t}.{raw_body} |

Method examples

HTML, URL, Markdown, template → PDF

await client.pdf.fromHtml("<h1>Hello</h1>", {
  options: { format: "letter", printBackground: true },
});

await client.pdf.fromUrl("https://example.com", {
  filename: "page.pdf",
  options: { format: "A4", waitUntil: "networkidle0" },
});

await client.pdf.fromMarkdown("# Hello\n\nFrom **Markdown**.");

await client.pdf.fromTemplate(
  "invoice",
  { number: "INV-1042", total: 1458 },
  { filename: "invoice.pdf", strict: true },
);

Private, loopback, and metadata hosts are rejected (url_not_allowed).

Headers and footers

Chromium placeholders: pageNumber, totalPages, date, title. Add top/bottom margin so they are not clipped.

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" },
  },
});

Screenshots

const shot = await client.images.fromUrl("https://example.com", {
  options: { fullPage: true, type: "png" },
});
await shot.save("page.png");

Convert (LibreOffice / wkhtml)

const fromWord = await client.convert.fromPath("./letter.docx", { to: "pdf" });
const docx = await client.convert.fromHtml("<h1>Report</h1>", { to: "docx" });
const wk = await client.convert.wkhtml({ html: "<h1>Legacy</h1>", toc: true });

to: pdf | docx | xlsx | html | png. Default engine is LibreOffice. First call after idle can take up to ~90s.

PDF tools

const pack = await client.pdf.merge({
  files: [
    { url: "https://example.com/cover.pdf" },
    { file: fromWord.kind === "binary" ? fromWord.bytes : new Uint8Array() },
  ],
});

await client.pdf.extract({ file: pack.bytes, pages: "1-2" });
await client.pdf.protect({ file: pack.bytes, userPassword: "secret" });
await client.pdf.stamp({ file: pack.bytes, text: "DRAFT", rotate: -24 });
await client.pdf.raster({ file: pack.bytes, pages: "1", type: "png", dpi: 150 });
await client.pdf.info({ file: pack.bytes });
await client.pdf.formFill({
  file: formBytes,
  fields: { Name: "Jane" },
  flatten: true,
});

Barcodes and zip

const qr = await client.barcodes.qr("https://relaypdf.com");
await client.barcodes.create({ type: "code128", text: "ABC-1042", format: "png" });
await client.zip.create({
  files: [
    { filename: "a.pdf", file: pdf.bytes },
    { filename: "b.pdf", url: "https://example.com/b.pdf" },
  ],
});

Barcode type: qr | code128 | code39 | ean13 | upca | pdf417 | datamatrix.

Templates

const gallery = await client.templates.gallery();
const created = await client.templates.create({
  name: "Invoice",
  html: "<h1>{{number}}</h1>",
  sampleData: { number: "INV-1" },
});
await client.templates.publish(created.id);

Account and health

const live = await client.health(); // { ok, service, requestId? }
const account = await client.account();
console.log(account.plan, account.rateTier, account.wallet.balanceMillicents);

Types

Exported from @relaypdf/sdk:

| Type | Role | |------|------| | RelayPDF | Client | | RelayPDFOptions | { apiKey, baseUrl?, fetch? } | | RelayPDFError | { status, code, message, retryAfter?, details? } | | GenerateResult | BinaryResult \| UrlResult \| AsyncResult | | BinaryResult | { kind: "binary", id, filename, sizeBytes, contentType, bytes, save } | | UrlResult | { kind: "url", id, status, url, filename, sizeBytes, expiresAt } | | AsyncResult | { kind: "async", id, status: "processing", pollUrl } | | Job | processing / completed / failed | | PdfOptions | Chromium print options | | ImageOptions | Screenshot options | | ResponseMode | "binary" \| "url" \| "async" | | Bookmark | { title, page } | | BarcodeType | qr, code128, … | | ConvertTarget | pdf | docx | xlsx | html | png | | ConvertEngine | libreoffice | wkhtmltopdf | | FileRef / NamedFileRef / BytesLike | File inputs |

id on binary results comes from x-relaypdf-id. sizeBytes from x-relaypdf-size when present. filename from content-disposition.

Webhooks

Dashboard endpoints receive job and wallet events. Manage them with client.webhooks.list() / create({ url, events? }) / delete(id). create returns secret once; list returns secretPrefix only. Signature header RelayPDF-Signature: t=<unix>,v1=<hex>. HMAC-SHA256 of {timestamp}.{raw_body} with the webhook secret (not the API key). Use the exact raw body string — do not JSON.parse and re-stringify.

import { verifyWebhook, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_EVENT_HEADER } from "@relaypdf/sdk";

const created = await client.webhooks.create({
  url: "https://example.com/hooks",
  events: ["job.completed", "job.failed"],
});
// created.secret is shown once — store it, then verify:

const ok = await verifyWebhook(
  process.env.RELAYPDF_WEBHOOK_SECRET!,
  rawBody,
  request.headers.get(WEBHOOK_SIGNATURE_HEADER) ?? "",
);

Default clock skew tolerance is 300 seconds. Events: job.completed, job.failed, wallet.topup, wallet.auto_reload, wallet.auto_reload_failed, wallet.payment_required.

Authorization

  • Type: HTTP Bearer (Authorization: Bearer pdf_live_…)
  • Where: every /v1/* call except GET /v1/files/:id
  • Not used: JWT. Keys are hashed at rest on the server.

Related docs

License

MIT. Legal entity: Strategic Products LLC, d/b/a RelayPDF.