PDFPipe

Guide

HTML to PDF in Deno

Deno has no good native PDF library, and installing Puppeteer or wkhtmltopdf via the npm compatibility layer adds heavy native dependencies that conflict with Deno's permission model. PDFPipe is a single HTTP call. Deno has fetch built in, so there is nothing to install and no permissions beyond --allow-net.

Handler that streams a PDF

Build the HTML, POST it to PDFPipe, and pipe the response body straight to the client. No temp files, no buffering:

typescript
// No imports needed — Deno has fetch and ReadableStream built in.
// Run with: PDFPIPE_API_KEY=pp_live_... deno run --allow-net --allow-env server.ts

const apiKey = Deno.env.get("PDFPIPE_API_KEY")!;

Deno.serve(async (req) => {
  const url = new URL(req.url);

  if (req.method === "GET" && url.pathname.startsWith("/invoices/")) {
    const id = url.pathname.split("/").pop();
    const html = `
      <html>
      <body style="font-family: system-ui; padding: 40px">
        <h1>Invoice #${id}</h1>
        <p>Thank you for your order.</p>
      </body>
      </html>
    `;

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

    if (!upstream.ok) {
      const err = await upstream.json().catch(() => ({}));
      return Response.json({ error: err.detail ?? "render failed" }, { status: 502 });
    }

    // Pipe the upstream body directly — no buffering.
    return new Response(upstream.body, {
      headers: {
        "Content-Type": "application/pdf",
        "Content-Disposition": `attachment; filename="invoice-${id}.pdf"`,
      },
    });
  }

  return new Response("Not found", { status: 404 });
});

Store the PDF and return a URL

Pass store: true to keep the document on the CDN. Useful for background tasks that generate PDFs and email a download link:

typescript
// Store the PDF and return a download URL.
// Useful for background tasks or when you need to email a link.
const upstream = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${Deno.env.get("PDFPIPE_API_KEY")}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    html,
    options: { format: "A4" },
    store: true,
    filename: `invoice-${orderId}.pdf`,
  }),
});

if (!upstream.ok) throw new Error("PDF render failed");

const docId  = upstream.headers.get("X-PDFPipe-Document-Id");
const docUrl = upstream.headers.get("X-PDFPipe-Document-Url");
const expiresAt = upstream.headers.get("X-PDFPipe-Document-Expires");

// Store docUrl in your database and email it to the customer.

Deno Deploy

The same code runs on Deno Deploy without modification. Set PDFPIPE_API_KEY in the project environment variables and deploy:

typescript
// Deno Deploy: same code, zero config. The env var is set in the
// Deno Deploy dashboard under Project → Settings → Environment Variables.

Deno.serve(async (req) => {
  if (req.method !== "POST") return new Response("Method not allowed", { status: 405 });

  const { html } = await req.json();

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

  return new Response(upstream.body, {
    headers: { "Content-Type": "application/pdf" },
  });
});

Getting an API key

The Hobby plan gives 500 free documents a month with no credit card required. Paid plans start at $19 and include a longer document archive, higher limits, and email support.

Full API reference on the docs page. Try it now in the live playground with no signup.

Get an API key →