Use case

Export dashboards to PDF
without Puppeteer

Your backend fetches the data, renders it to HTML, and POSTs to the API. Users get a PDF download button. No Chromium on your server.

Start free

Why Puppeteer breaks on serverless

AWS Lambda

Chromium exceeds the 250 MB unzipped code limit. Requires a Lambda Layer that is painful to keep updated.

Vercel Edge

Edge Functions block Node.js native modules. Puppeteer cannot run here at all.

Cloudflare Workers

No file system, no native binaries. Puppeteer is completely incompatible.

How it works

The pattern for private dashboards: your API route fetches the same data the dashboard displays, builds an HTML template from it, then calls the PDF API. The user gets a file download. No browser, no auth tokens passed to a third party.

Next.js API route: fetch data, build HTML, return PDF
export async function POST(request: Request) {
  const { reportId } = await request.json() as { reportId: string }

  // 1. Fetch your data server-side (auth handled here)
  const data = await fetchReportData(reportId)

  // 2. Build the HTML from your data
  const html = `
    <!DOCTYPE html>
    <html>
    <head>
      <style>
        body { font-family: sans-serif; padding: 32px; }
        table { width: 100%; border-collapse: collapse; }
        th, td { padding: 8px 12px; border-bottom: 1px solid #e5e7eb; text-align: left; }
        th { background: #f8fafc; font-weight: 600; }
      </style>
    </head>
    <body>
      <h1>${data.title}</h1>
      <p>Period: ${data.period}</p>
      <table>
        <thead><tr>${data.columns.map((c: string) => `<th>${c}</th>`).join('')}</tr></thead>
        <tbody>${data.rows.map((r: string[]) => `<tr>${r.map(v => `<td>${v}</td>`).join('')}</tr>`).join('')}</tbody>
      </table>
    </body>
    </html>
  `

  // 3. Generate the PDF
  const res = await fetch('https://platform.htmltopdfapi.co/api/v1/pdf/generate', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.HTMLTOPDF_API_KEY!}`,
      'Content-Type': 'application/json',
      Accept: 'application/pdf',
    },
    body: JSON.stringify({ html, paper_size: 'a4', orientation: 'landscape' }),
  })

  if (!res.ok) return Response.json({ error: 'Export failed' }, { status: 500 })

  return new Response(await res.arrayBuffer(), {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': 'attachment; filename="dashboard.pdf"',
    },
  })
}
React export button
async function exportDashboardPdf() {
  const res = await fetch('/api/export-pdf', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ reportId: currentReportId }),
  })
  if (!res.ok) throw new Error('Export failed')

  const blob = await res.blob()
  const link = document.createElement('a')
  link.href = URL.createObjectURL(blob)
  link.download = 'dashboard.pdf'
  link.click()
  URL.revokeObjectURL(link.href)
}

Public or internal dashboards

If your dashboard is publicly accessible (or behind a token-based preview URL), you can pass the URL directly instead of building HTML server-side. Use wait_until: networkidle0 to ensure JS-rendered charts finish loading before the PDF is captured.

URL-based approach for public dashboards
{
  "url": "https://your-app.com/dashboard/report?token=preview_abc123",
  "paper_size": "a4",
  "orientation": "landscape",
  "wait_until": "networkidle0"
}

Features

Works on Vercel, Lambda, Cloudflare
No binary, no Docker changes
Landscape and portrait orientation
Full CSS Grid and flexbox for layouts
SVG charts render correctly
wait_until for JS-rendered charts
Server-side HTML for private dashboards
URL-based for public pages
Free tier: 200 pages/day

Add dashboard PDF export free

200 pages/day on the free tier. No credit card required.

Get your free API key