PDFPipe

Guide

HTML to PDF in GitHub Actions

Generating PDFs in CI lets you produce report files on push, attach release notes to GitHub Releases, or build documentation artifacts as part of a publish pipeline. The traditional approach requires installing headless Chrome, wkhtmltopdf, or WeasyPrint in the runner image, which adds minutes to install time and megabytes of cached state. PDFPipe is a single HTTP call with no runner dependencies.

Why not just install Puppeteer in CI?

ApproachSetup timeCache sizeComplexity
apt install chromium~45 sNo cacheFails on arm runners
npx puppeteer + chrome~60 s~400 MBNeeds --no-sandbox
wkhtmltopdf~20 s~100 MBOld engine, CSS gaps
PDFPipe API0 s0 bytesJust curl + a secret

Step 1: Add the API key as a secret

Get a free API key at pdfpipe.xyz/signup. Add it to your repository at Settings → Secrets and variables → Actions → New repository secret. Name it PDFPIPE_API_KEY.

Basic workflow: render and upload artifact

This workflow renders an HTML file and uploads the PDF as a build artifact, downloadable from the Actions run page.

.github/workflows/pdf.yml
.github/workflows/pdf.yml

name: Generate PDF

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  generate-pdf:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Generate PDF from HTML file
        run: |
          curl -s -X POST https://api.pdfpipe.xyz/v1/pdf \
            -H "Authorization: Bearer ${{ secrets.PDFPIPE_API_KEY }}" \
            -H "Content-Type: application/json" \
            -d "{\"html\": \"$(cat report.html | jq -Rs .)\"}" \
            --output report.pdf

      - name: Upload PDF artifact
        uses: actions/upload-artifact@v4
        with:
          name: report-pdf
          path: report.pdf
          retention-days: 30

Using a Node.js script

For complex templates or multi-page documents, a Node.js script gives you full control over the HTML before sending it to the API:

scripts/generate-pdf.mjs
// scripts/generate-pdf.mjs
// Runs in a GitHub Actions step: node scripts/generate-pdf.mjs

import { readFileSync, writeFileSync } from "fs";

const html = readFileSync("report.html", "utf-8");

const resp = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PDFPIPE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    html,
    options: {
      format: "A4",
      margin: { top: "20mm", bottom: "20mm", left: "15mm", right: "15mm" },
      print_background: true,
    },
  }),
});

if (!resp.ok) {
  const err = await resp.text();
  console.error("PDF generation failed:", err);
  process.exit(1);
}

const buffer = Buffer.from(await resp.arrayBuffer());
writeFileSync("report.pdf", buffer);
console.log(`Generated report.pdf (${buffer.length} bytes)`);

In your workflow, call it with node scripts/generate-pdf.mjs and set PDFPIPE_API_KEY: ${{ secrets.PDFPIPE_API_KEY }} in the step's env.

Attach a PDF to a GitHub Release

Trigger on the release: published event to automatically attach a rendered document to every release:

.github/workflows/release-pdf.yml
.github/workflows/release-pdf.yml

name: Attach PDF to release

on:
  release:
    types: [published]

jobs:
  generate-and-attach:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4

      - name: Build HTML (example: use Node to compile a template)
        run: node scripts/build-release-notes.js > release.html

      - name: Render to PDF
        run: |
          curl -s -X POST https://api.pdfpipe.xyz/v1/pdf \
            -H "Authorization: Bearer ${{ secrets.PDFPIPE_API_KEY }}" \
            -H "Content-Type: application/json" \
            -d '{"html": "'$(cat release.html | base64 -w0)'", "options": {"format":"A4"}}' \
            --output release-notes.pdf

      - name: Attach PDF to GitHub Release
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh release upload ${{ github.event.release.tag_name }} release-notes.pdf

Store the PDF and post a URL

Pass "store": true to get back a stable URL instead of raw bytes. The URL can be shared in a Slack message, email, or PR comment. Document retention depends on your plan.

workflow snippet
# Store the PDF and post its URL to a Slack channel

      - name: Render and store PDF
        id: pdf
        run: |
          RESPONSE=$(curl -s -X POST https://api.pdfpipe.xyz/v1/pdf \
            -H "Authorization: Bearer ${{ secrets.PDFPIPE_API_KEY }}" \
            -H "Content-Type: application/json" \
            -d '{"html":"...","store":true,"options":{"format":"A4"}}')
          echo "url=$(echo $RESPONSE | jq -r '.document_url // empty')" >> $GITHUB_OUTPUT

      # Note: "store":true returns JSON instead of raw bytes.
      # The document URL is stable until the plan's retention window expires.

      - name: Post to Slack
        if: steps.pdf.outputs.url != ''
        run: |
          curl -s -X POST ${{ secrets.SLACK_WEBHOOK }} \
            -H "Content-Type: application/json" \
            -d '{"text":"Report ready: ${{ steps.pdf.outputs.url }}"}'

Common questions

Does this work on arm64 / M1 runners?

Yes. PDFPipe is a remote API call so it has no native binary dependencies. It runs identically on ubuntu, macos, windows, and arm GitHub-hosted runners.

How do I handle large HTML files?

The API accepts up to 5 MB of HTML per request. For larger documents, split them into multiple requests or use the "url" field to point the renderer at a served URL rather than inlining the HTML.

Can I render from a URL that requires authentication?

The renderer runs in an isolated environment without your session cookies. Use the "html" field instead: generate the HTML server-side in your script and send it inline. That way, authentication happens in your workflow, not the renderer.

Get started

500 renders a month free. Key issued instantly, no card needed.