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

@pdfbrew/sdk

v0.2.0

Published

TypeScript SDK for the pdfbrew PDF rendering API

Downloads

122

Readme

@pdfbrew/sdk

TypeScript SDK for pdfbrew — PDF infrastructure for developers.

Render HTML, Markdown, or URLs to pixel-perfect PDFs with a single API call. Cached, async, with webhooks and custom fonts.

Installation

npm install @pdfbrew/sdk

Quick start

import { PdfBrew } from '@pdfbrew/sdk'

const client = new PdfBrew('pdfbrew_live_...')

// HTML → PDF
const render = await client.createRender({
  source: { type: 'html', html: '<h1>Invoice #42</h1>' },
  options: { format: 'A4' },
  async: false,
})

console.log(render.output.url)   // signed 1-hour download URL
console.log(render.cache_hit)    // true if served from cache instantly

Rendering

HTML → PDF

const render = await client.createRender({
  source: {
    type: 'html',
    html: '<h1>Hello</h1>',
    css: 'body { font-family: Inter; }',  // optional extra CSS
  },
  options: {
    format: 'A4',           // A4 | A3 | Letter | Legal
    margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
    landscape: false,
    print_background: true,
  },
  async: false,
})
// render.status === 'completed'
// render.output.url    — signed download URL (1-hour TTL)
// render.output.pages
// render.output.bytes
// render.render_duration_ms

Markdown → PDF

Markdown is automatically converted to styled HTML before rendering. No extra setup needed.

const render = await client.createRender({
  source: {
    type: 'markdown',
    markdown: '# Invoice #42\n\n| Item | Price |\n|---|---|\n| Widget | $10 |',
    css: 'body { max-width: 800px; }',  // optional CSS override
  },
  async: false,
})

URL → PDF

const render = await client.createRender({
  source: { type: 'url', url: 'https://example.com' },
  options: {
    wait_until: 'networkidle0',
    wait_for_selector: '#report-ready',  // wait for this element before printing
    headers: { Authorization: 'Bearer token' },  // extra HTTP headers
  },
  async: false,
})

Async rendering with webhook

const render = await client.createRender({
  source: { type: 'html', html: '<h1>Hello</h1>' },
  async: true,
  webhook_url: 'https://your-app.com/webhooks/pdf',
})
// render.status === 'queued'
// Your webhook receives a POST when the PDF is ready

Idempotent requests

Pass an idempotency key as the second argument to safely retry without generating duplicate PDFs:

const render = await client.createRender(
  { source: { type: 'html', html: '<h1>Invoice #42</h1>' }, async: false },
  'inv_42',  // idempotency key
)

Bypass cache

const render = await client.createRender({
  source: { type: 'html', html: '<h1>Always fresh</h1>' },
  force_render: true,
})

Get a render

const render = await client.getRender('rnd_abc123')
console.log(render.output.url) // fresh signed URL every time

List renders

const list = await client.listRenders({ limit: 20, status: 'completed' })

for (const item of list.data) {
  console.log(item.id, item.status, item.source_type, item.render_duration_ms)
}

// Paginate
if (list.has_more) {
  const next = await client.listRenders({ before: list.next_before })
}

Custom fonts

Upload fonts once. They are automatically injected as @font-face CSS into every render. Reference them by family name in your HTML/CSS.

import { readFileSync } from 'fs'

// Upload
const font = await client.uploadFont(
  'Inter',
  new Blob([readFileSync('./Inter-Regular.woff2')], { type: 'font/woff2' }),
  'Inter-Regular.woff2',
)

// Use in HTML
await client.createRender({
  source: { type: 'html', html: '<p style="font-family: Inter">Hello</p>' },
  async: false,
})

// List uploaded fonts
const fonts = await client.listFonts()

// Remove a font
await client.deleteFont(font.id)

API key management

// Create a key (raw key value shown once — store it securely)
const { key, ...meta } = await client.createKey('Production', 'live')

// List keys
const keys = await client.listKeys()

// Revoke a key
await client.deleteKey('key_abc123')

Webhook endpoint management

// Register an endpoint (secret shown once — store it to verify payloads)
const endpoint = await client.createWebhook(
  'https://your-app.com/webhooks/pdf',
  ['render.completed', 'render.failed'],
)
console.log(endpoint.secret) // whsec_...

// List endpoints
const webhooks = await client.listWebhooks()

// Delete an endpoint
await client.deleteWebhook('whk_abc123')

Webhook payload verification

import { createHmac } from 'crypto'

function verifyWebhook(rawBody: string, signature: string, secret: string): boolean {
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex')
  return signature === expected
}

// In your handler:
const valid = verifyWebhook(
  JSON.stringify(req.body),
  req.headers['x-pdfbrew-signature'],
  process.env.WHSEC!,
)

Stats & settings

// Usage stats
const stats = await client.getStats()
console.log(stats.total, stats.today, stats.this_month)
console.log(stats.daily) // 30-day chart

// Account settings
const settings = await client.getSettings()
console.log(settings.plan, settings.retention_days)

// Update PDF retention (1–30 days)
await client.updateSettings(14)

Billing

// Current plan and subscription status
const billing = await client.getBillingStatus()
// { plan: 'pro', subscription_status: 'active', dodo_subscription_id: 'sub_...' }

// Create a checkout session (redirect user to checkout_url)
const { checkout_url } = await client.createCheckout('pro')

// Switch between paid plans (no checkout needed)
await client.changePlan('business')

// Cancel and downgrade to free
await client.cancelSubscription()

Error handling

import { PdfBrew, PdfBrewError } from '@pdfbrew/sdk'

try {
  const render = await client.createRender({ ... })
} catch (e) {
  if (e instanceof PdfBrewError) {
    console.log(e.code)    // 'rate_limited' | 'quota_exceeded' | 'concurrent_limit' | ...
    console.log(e.status)  // HTTP status code
    console.log(e.message)
  }
}

Error codes: invalid_source, invalid_options, invalid_request, render_timeout, render_failed, rate_limited, quota_exceeded, concurrent_limit, unauthorized, not_found, idempotency_conflict, internal_error.

Plan limits

| Plan | Monthly renders | Concurrent renders | |---|---|---| | Free | 100 | 2 | | Pro | 5,000 | 5 | | Business | 50,000 | 10 |

Exceeding the monthly quota returns a quota_exceeded error (429). Exceeding the concurrent cap returns concurrent_limit (429).

Runtime support

Works in Node.js, Deno, Bun, Cloudflare Workers, and the browser — uses the standard fetch API and FormData.

Links

License

MIT