@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
Maintainers
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.
- Docs: relaypdf.com/docs/sdks/node
- REST reference: relaypdf.com/docs
- OpenAPI: relaypdf.com/openapi.json
- Support: [email protected]
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.
- Sign in at relaypdf.com.
- Open Dashboard → API keys and create a key, or run
npx @relaypdf/cli setupand click Approve. - 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/sdkpnpm add @relaypdf/sdkyarn add @relaypdf/sdkFrom git (monorepo path packages/sdk):
npm install github:timspell1/PDF#mainThe 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 exceptGET /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.
