Pay-per-use · No subscriptions

HTML to PDF in Node.js — without bundling a browser

One fetch call. Zero Chromium in your deployment. Pay per page.

Render PDFs from any Node.js runtime — Express, Next.js API routes, NestJS, Fastify, Koa, AWS Lambda, Vercel Functions, Cloudflare Workers — with a single HTTP request. Skip the 200 MB Puppeteer download, the ARM/x86 binary mismatches, and the memory spikes that kill your serverless function.

Why developers choose HTML2DocHub

Works on any Node runtime — Node 18+, Deno, Bun, Cloudflare Workers
Native fetch (no dependencies) or your favourite client: axios, got, ky, undici
No Chromium in your bundle — saves 200 MB and the cold-start penalty
Serverless-ready — Vercel, Netlify, Lambda, Cloud Run, Workers
TypeScript-friendly — plain JSON request/response, no .d.ts headaches
Idempotency keys for safe retries inside queues and job runners
Async mode + webhook callback for reports that render for minutes
Pay ₹0.08 per page — no subscription floor

Code Examples

Native fetch (Node 18+, Next.js, Bun)typescript
const API_KEY = process.env.HTML2DOCHUB_API_KEY!;

export async function renderPdf(html: string): Promise<Buffer> {
  const res = await fetch("https://api.html2dochub.com/v1/render", {
    method: "POST",
    headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({
      type: "pdf",
      html,
      options: { format: "A4", margin: "18mm" },
    }),
  });
  if (!res.ok) throw new Error(`Render failed: ${res.status}`);
  const job = await res.json();

  const pdfRes = await fetch(job.download_url);
  return Buffer.from(await pdfRes.arrayBuffer());
}
Next.js App Router — streaming PDF responsetypescript
// app/api/invoice/[id]/pdf/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function GET(
  _req: NextRequest,
  { params }: { params: { id: string } },
) {
  const invoice = await getInvoice(params.id);
  const html = renderInvoiceTemplate(invoice);

  const res = await fetch("https://api.html2dochub.com/v1/render", {
    method: "POST",
    headers: {
      "X-API-Key": process.env.HTML2DOCHUB_API_KEY!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ type: "pdf", html, options: { format: "A4" } }),
  });
  const job = await res.json();
  const pdf = await fetch(job.download_url).then((r) => r.arrayBuffer());

  return new NextResponse(pdf, {
    headers: {
      "Content-Type": "application/pdf",
      "Content-Disposition": `attachment; filename="invoice-${params.id}.pdf"`,
    },
  });
}
Express — download routejavascript
const express = require("express");
const app = express();

app.get("/invoice/:id.pdf", async (req, res) => {
  const invoice = await Invoice.findById(req.params.id);
  const html = renderTemplate("invoice", { invoice });

  const apiRes = await fetch("https://api.html2dochub.com/v1/render", {
    method: "POST",
    headers: {
      "X-API-Key": process.env.HTML2DOCHUB_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ type: "pdf", html, options: { format: "A4" } }),
  });
  const job = await apiRes.json();

  const pdfRes = await fetch(job.download_url);
  res.set("Content-Type", "application/pdf");
  res.set("Content-Disposition", `attachment; filename="invoice-${req.params.id}.pdf"`);
  pdfRes.body.pipe(res);
});

app.listen(3000);
BullMQ worker — async render with retriestypescript
import { Worker, Queue } from "bullmq";

export const reportQueue = new Queue("reports");

new Worker("reports", async (job) => {
  const { reportId, userId } = job.data;
  const idempotencyKey = `report:${reportId}:v${job.attemptsMade}`;

  const res = await fetch("https://api.html2dochub.com/v1/render", {
    method: "POST",
    headers: {
      "X-API-Key": process.env.HTML2DOCHUB_API_KEY!,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      type: "pdf",
      mode: "async",
      html: await buildReportHtml(reportId),
      webhook_url: `${process.env.BASE_URL}/webhooks/pdf-ready`,
      tag: `user:${userId}`,
      idempotency_key: idempotencyKey,
    }),
  });
  if (!res.ok) throw new Error(`Render enqueue failed: ${res.status}`);

  const { id: html2DochubJobId } = await res.json();
  await db.report.update({ where: { id: reportId }, data: { html2DochubJobId } });
}, { concurrency: 4 });

Simple, transparent pricing

Pay only for pages rendered. No subscriptions. No minimum monthly fee.

1 page PDF:~₹0.10
10 page PDF:~₹0.80
100 pages/day:~₹8/day
See full pricing details

Frequently Asked Questions

How does this compare to running Puppeteer myself?+
Puppeteer gives you maximum control but you own the operational burden: Chromium binary (200 MB+), memory spikes, sandbox flags, zombie process cleanup, ARM/x86 variants, cold-start penalties in serverless. HTML2DocHub runs the same Chromium on our side — you send HTML, we return a PDF. If you just want output, calling an API is usually 10x simpler than self-hosting.
What about puppeteer-core + @sparticuz/chromium for Lambda?+
That stack works but you're responsible for: keeping up with Chromium security patches, tuning the Lambda memory size (PDFs routinely OOM at 1 GB), handling cold starts on a 50 MB zip upload. HTML2DocHub sidesteps all of it — a Lambda calling our API just needs the fetch built-in (Node 18+).
Does it work on Vercel / Netlify / Cloudflare Workers?+
Yes on all three. It's an HTTPS call from your serverless function. Cloudflare Workers in particular is impossible with self-hosted Chromium — you need a remote rendering service.
Is there an official Node.js SDK?+
Not yet — the API is a single POST, so a 10-line wrapper in your own codebase covers most usage. An official @html2dochub/client npm package is planned for 2026 Q3.
How do I handle large reports that take minutes to render?+
Use async mode. Set "mode": "async" and pass a webhook_url. The request returns immediately; when rendering finishes, we POST the completed job (with download_url) to your webhook. Keep a BullMQ/Cloud Tasks job open and resolve it on the webhook.

Start rendering PDFs today

Free account. No credit card required. API ready in minutes.

Get your free API key