PDFPipe

Guide

HTML to PDF in Cloudflare Workers

Cloudflare Workers run in V8 isolates with no Node.js APIs, no file system, and no support for native binaries. Running a headless browser inside a Worker is not possible. PDF generation in Workers has traditionally meant either writing raw PDF commands yourself or calling an external service. This guide shows how to call the PDFPipe API from a Worker in a few lines, stream the result back to the caller, or store it in R2.

Prerequisites

  • A PDFPipe API key, stored as a Worker secret: wrangler secret put PDFPIPE_API_KEY
  • The key declared in your wrangler.toml under [vars] (for non-secrets) or accessed from env

Streaming a PDF response

src/index.ts
export interface Env {
  PDFPIPE_API_KEY: string;
}

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    if (req.method !== "POST") {
      return new Response("Method not allowed", { status: 405 });
    }

    const { html, filename = "document.pdf" } = await req.json<{
      html: string;
      filename?: string;
    }>();

    const res = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${env.PDFPIPE_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ html, options: { format: "A4" } }),
    });

    if (!res.ok) {
      const { detail } = await res.json<{ detail: string }>();
      return new Response(detail, { status: 502 });
    }

    return new Response(res.body, {
      headers: {
        "Content-Type": "application/pdf",
        "Content-Disposition": `attachment; filename="${filename}"`,
      },
    });
  },
};

Storing the PDF in R2

For async generation, render the PDF and write it to R2. Other Workers or services can retrieve it by key.

src/index.ts: R2 variant
export interface Env {
  PDFPIPE_API_KEY: string;
  BUCKET: R2Bucket;
}

export default {
  async fetch(req: Request, env: Env): Promise<Response> {
    const { html, key } = await req.json<{ html: string; key: string }>();

    const res = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${env.PDFPIPE_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ html, options: { format: "A4" } }),
    });

    if (!res.ok) {
      const { detail } = await res.json<{ detail: string }>();
      return Response.json({ error: detail }, { status: 502 });
    }

    await env.BUCKET.put(key, res.body, {
      httpMetadata: { contentType: "application/pdf" },
    });

    return Response.json({ key });
  },
};

Using the PDFPipe document store

Alternatively, pass store: true in the request body and PDFPipe stores the PDF and returns a CDN URL. This removes the need for an R2 bucket entirely for simple link-sharing scenarios.

src/index.ts: stored document variant
const res = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${env.PDFPIPE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    html,
    options: { format: "A4" },
    store: true,
    filename: "invoice.pdf",
  }),
});

const { id, url } = await res.json<{ id: string; url: string }>();
return Response.json({ id, url });

Hono integration

If your Worker uses Hono for routing, the same fetch call works inside a route handler.

src/index.ts (Hono)
import { Hono } from "hono";

type Bindings = { PDFPIPE_API_KEY: string };

const app = new Hono<{ Bindings: Bindings }>();

app.post("/invoice/:id", async (c) => {
  const orderId = c.req.param("id");
  const html = `<h1>Invoice #${orderId}</h1>`;

  const res = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${c.env.PDFPIPE_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ html, options: { format: "A4" } }),
  });

  if (!res.ok) {
    const { detail } = await res.json<{ detail: string }>();
    return c.text(detail, 502);
  }

  return new Response(res.body, {
    headers: {
      "Content-Type": "application/pdf",
      "Content-Disposition": `attachment; filename="invoice-${orderId}.pdf"`,
    },
  });
});

export default app;

Get started free

500 documents a month, no card required. Your API key is issued immediately after signup.

Get your API key →