Tutorial PDF

Generate PDFs from Any Webpage Using an API

February 6, 2026 ยท 4 min read

Stack of documents and journal representing PDF generation

Photo via Unsplash

PDF generation is one of the most common developer tasks. Invoices, reports, receipts, contracts, articles saved for offline reading -- PDFs are everywhere. But generating them programmatically has always been painful. Libraries like wkhtmltopdf are outdated, Puppeteer requires managing browser infrastructure, and most PDF libraries cannot render modern CSS. SnapAPI makes it simple: send a URL or HTML, get back a PDF rendered in a real Chromium browser.

Why Use an API for PDF Generation?

Basic URL to PDF with curl

The simplest way to generate a PDF. Just set format: "pdf":

curl -X POST https://api.snapapi.pics/v1/screenshot \
  -H "X-Api-Key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://en.wikipedia.org/wiki/PDF",
    "format": "pdf",
    "responseType": "image"
  }' \
  --output article.pdf

This captures the entire page and saves it as a properly formatted PDF document. The responseType: "image" returns raw binary data (in this case, the PDF file itself) rather than a JSON-wrapped base64 string.

URL to PDF with Node.js

Here is a complete Node.js example:

const fs = require("fs");

const SNAPAPI_KEY = "your_api_key";

async function urlToPdf(url, outputPath = "output.pdf") {
  const response = await fetch("https://api.snapapi.pics/v1/screenshot", {
    method: "POST",
    headers: {
      "X-Api-Key": SNAPAPI_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      url,
      format: "pdf",
      fullPage: true,
      blockCookieBanners: true,
      responseType: "image",
    }),
  });

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`PDF generation failed: ${response.status} ${error}`);
  }

  const buffer = Buffer.from(await response.arrayBuffer());
  fs.writeFileSync(outputPath, buffer);
  console.log(`PDF saved: ${outputPath} (${buffer.length} bytes)`);
  return buffer;
}

// Generate PDF from a webpage
urlToPdf("https://stripe.com/docs/api", "stripe-api-docs.pdf");

URL to PDF with Python

import requests

SNAPAPI_KEY = "your_api_key"

def url_to_pdf(url: str, output_path: str = "output.pdf") -> int:
    response = requests.post(
        "https://api.snapapi.pics/v1/screenshot",
        headers={
            "X-Api-Key": SNAPAPI_KEY,
            "Content-Type": "application/json",
        },
        json={
            "url": url,
            "format": "pdf",
            "fullPage": True,
            "blockCookieBanners": True,
            "responseType": "image",
        },
        timeout=30,
    )
    response.raise_for_status()

    with open(output_path, "wb") as f:
        f.write(response.content)

    print(f"PDF saved: {output_path} ({len(response.content)} bytes)")
    return len(response.content)


# Generate PDF from any URL
url_to_pdf("https://news.ycombinator.com", "hackernews.pdf")

HTML to PDF: Invoice Template

The real power of PDF generation comes from rendering custom HTML. Here is a complete invoice template that you can populate with dynamic data:

function generateInvoiceHtml(invoice) {
  const rows = invoice.items
    .map(
      (item) => `
    <tr>
      <td>${item.description}</td>
      <td style="text-align: center;">${item.quantity}</td>
      <td style="text-align: right;">$${item.unitPrice.toFixed(2)}</td>
      <td style="text-align: right;">$${(item.quantity * item.unitPrice).toFixed(2)}</td>
    </tr>
  `
    )
    .join("");

  const subtotal = invoice.items.reduce(
    (sum, item) => sum + item.quantity * item.unitPrice, 0
  );
  const tax = subtotal * (invoice.taxRate || 0);
  const total = subtotal + tax;

  return `
<!DOCTYPE html>
<html>
<head>
  <style>
    @page { size: A4; margin: 0; }
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      color: #1a1a2e;
      padding: 60px;
      font-size: 14px;
      line-height: 1.5;
    }
    .header {
      display: flex;
      justify-content: space-between;
      margin-bottom: 50px;
    }
    .company-name {
      font-size: 28px;
      font-weight: 800;
      color: #8b5cf6;
    }
    .invoice-label {
      font-size: 36px;
      font-weight: 300;
      color: #94a3b8;
      text-align: right;
    }
    .invoice-number {
      font-size: 14px;
      color: #64748b;
      text-align: right;
      margin-top: 4px;
    }
    .addresses {
      display: flex;
      justify-content: space-between;
      margin-bottom: 40px;
    }
    .address-block h4 {
      font-size: 12px;
      text-transform: uppercase;
      color: #94a3b8;
      margin-bottom: 8px;
      letter-spacing: 0.5px;
    }
    .address-block p {
      color: #334155;
      line-height: 1.6;
    }
    table {
      width: 100%;
      border-collapse: collapse;
      margin-bottom: 30px;
    }
    th {
      background: #f8fafc;
      padding: 12px 16px;
      text-align: left;
      font-size: 12px;
      text-transform: uppercase;
      color: #64748b;
      border-bottom: 2px solid #e2e8f0;
      letter-spacing: 0.5px;
    }
    td {
      padding: 14px 16px;
      border-bottom: 1px solid #f1f5f9;
      color: #334155;
    }
    .totals {
      display: flex;
      justify-content: flex-end;
    }
    .totals-table {
      width: 280px;
    }
    .totals-table tr td {
      border: none;
      padding: 6px 16px;
    }
    .totals-table .total-row {
      font-size: 18px;
      font-weight: 700;
      color: #1a1a2e;
      border-top: 2px solid #1a1a2e;
    }
    .footer {
      position: absolute;
      bottom: 60px;
      left: 60px;
      right: 60px;
      text-align: center;
      color: #94a3b8;
      font-size: 12px;
      border-top: 1px solid #e2e8f0;
      padding-top: 20px;
    }
  </style>
</head>
<body>
  <div class="header">
    <div>
      <div class="company-name">${invoice.company.name}</div>
      <p style="color: #64748b; margin-top: 4px;">${invoice.company.address}</p>
    </div>
    <div>
      <div class="invoice-label">INVOICE</div>
      <div class="invoice-number">#${invoice.number}</div>
      <div class="invoice-number">Date: ${invoice.date}</div>
      <div class="invoice-number">Due: ${invoice.dueDate}</div>
    </div>
  </div>

  <div class="addresses">
    <div class="address-block">
      <h4>Bill To</h4>
      <p><strong>${invoice.client.name}</strong><br>
      ${invoice.client.address}<br>
      ${invoice.client.email}</p>
    </div>
  </div>

  <table>
    <thead>
      <tr>
        <th>Description</th>
        <th style="text-align: center;">Qty</th>
        <th style="text-align: right;">Unit Price</th>
        <th style="text-align: right;">Amount</th>
      </tr>
    </thead>
    <tbody>
      ${rows}
    </tbody>
  </table>

  <div class="totals">
    <table class="totals-table">
      <tr>
        <td>Subtotal</td>
        <td style="text-align: right;">$${subtotal.toFixed(2)}</td>
      </tr>
      <tr>
        <td>Tax (${((invoice.taxRate || 0) * 100).toFixed(0)}%)</td>
        <td style="text-align: right;">$${tax.toFixed(2)}</td>
      </tr>
      <tr class="total-row">
        <td>Total</td>
        <td style="text-align: right;">$${total.toFixed(2)}</td>
      </tr>
    </table>
  </div>

  <div class="footer">
    Thank you for your business. Payment is due within 30 days.
  </div>
</body>
</html>
  `;
}

Generate the Invoice PDF

async function generateInvoicePdf(invoice) {
  const html = generateInvoiceHtml(invoice);

  const response = await fetch("https://api.snapapi.pics/v1/screenshot", {
    method: "POST",
    headers: {
      "X-Api-Key": "your_api_key",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      html,
      format: "pdf",
      width: 794,   // A4 width in pixels at 96 DPI
      height: 1123,  // A4 height in pixels at 96 DPI
      responseType: "image",
    }),
  });

  if (!response.ok) throw new Error(`Failed: ${response.status}`);
  return Buffer.from(await response.arrayBuffer());
}

// Usage
const invoice = {
  number: "INV-2026-0042",
  date: "February 6, 2026",
  dueDate: "March 8, 2026",
  company: {
    name: "Acme Corp",
    address: "123 Business St, San Francisco, CA 94102",
  },
  client: {
    name: "Jane Smith",
    address: "456 Client Ave, New York, NY 10001",
    email: "jane@example.com",
  },
  items: [
    { description: "Web Development - Homepage Redesign", quantity: 40, unitPrice: 150 },
    { description: "API Integration", quantity: 16, unitPrice: 175 },
    { description: "QA Testing & Bug Fixes", quantity: 8, unitPrice: 125 },
  ],
  taxRate: 0.08,
};

const pdf = await generateInvoicePdf(invoice);
fs.writeFileSync(`${invoice.number}.pdf`, pdf);
console.log(`Invoice PDF generated: ${invoice.number}.pdf`);

Full-Page PDF

By default, a PDF capture renders the visible viewport. To capture the entire scrollable page as a continuous PDF, use fullPage: true:

curl -X POST https://api.snapapi.pics/v1/screenshot \
  -H "X-Api-Key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://tailwindcss.com/docs/installation",
    "format": "pdf",
    "fullPage": true,
    "blockCookieBanners": true,
    "responseType": "image"
  }' \
  --output tailwind-docs.pdf

This is particularly useful for generating PDF copies of documentation pages, long-form articles, or legal documents.

Python: Generate Invoice PDFs

Here is the same invoice generation flow in Python:

import requests

SNAPAPI_KEY = "your_api_key"

def generate_invoice_pdf(invoice: dict, output_path: str) -> None:
    # Build line items HTML
    rows = ""
    subtotal = 0
    for item in invoice["items"]:
        amount = item["quantity"] * item["unit_price"]
        subtotal += amount
        rows += f"""
        <tr>
            <td>{item["description"]}</td>
            <td style="text-align: center;">{item["quantity"]}</td>
            <td style="text-align: right;">${item["unit_price"]:.2f}</td>
            <td style="text-align: right;">${amount:.2f}</td>
        </tr>"""

    tax = subtotal * invoice.get("tax_rate", 0)
    total = subtotal + tax

    html = f"""<!DOCTYPE html>
<html>
<head>
  <style>
    body {{ font-family: system-ui; padding: 60px; color: #1a1a2e; font-size: 14px; }}
    table {{ width: 100%; border-collapse: collapse; }}
    th {{ background: #f8fafc; padding: 12px; text-align: left; border-bottom: 2px solid #e2e8f0; }}
    td {{ padding: 12px; border-bottom: 1px solid #f1f5f9; }}
    h1 {{ color: #8b5cf6; font-size: 24px; }}
  </style>
</head>
<body>
  <h1>{invoice["company"]}</h1>
  <p style="color: #64748b;">Invoice #{invoice["number"]} | {invoice["date"]}</p>
  <hr style="margin: 20px 0; border-color: #e2e8f0;">
  <p><strong>Bill To:</strong> {invoice["client"]}</p>

  <table style="margin-top: 30px;">
    <thead>
      <tr>
        <th>Description</th>
        <th style="text-align: center;">Qty</th>
        <th style="text-align: right;">Rate</th>
        <th style="text-align: right;">Amount</th>
      </tr>
    </thead>
    <tbody>{rows}</tbody>
  </table>

  <div style="text-align: right; margin-top: 20px;">
    <p>Subtotal: ${subtotal:.2f}</p>
    <p>Tax ({invoice.get("tax_rate", 0) * 100:.0f}%): ${tax:.2f}</p>
    <p style="font-size: 20px; font-weight: 700; margin-top: 8px;">Total: ${total:.2f}</p>
  </div>
</body>
</html>"""

    response = requests.post(
        "https://api.snapapi.pics/v1/screenshot",
        headers={
            "X-Api-Key": SNAPAPI_KEY,
            "Content-Type": "application/json",
        },
        json={
            "html": html,
            "format": "pdf",
            "width": 794,
            "height": 1123,
            "responseType": "image",
        },
        timeout=30,
    )
    response.raise_for_status()

    with open(output_path, "wb") as f:
        f.write(response.content)

    print(f"Invoice PDF saved: {output_path} ({len(response.content)} bytes)")


# Generate an invoice
generate_invoice_pdf(
    {
        "number": "INV-2026-0042",
        "date": "February 6, 2026",
        "company": "Acme Corp",
        "client": "Jane Smith, jane@example.com",
        "tax_rate": 0.08,
        "items": [
            {"description": "Web Development", "quantity": 40, "unit_price": 150},
            {"description": "API Integration", "quantity": 16, "unit_price": 175},
            {"description": "QA Testing", "quantity": 8, "unit_price": 125},
        ],
    },
    "invoice.pdf",
)

Express.js PDF Endpoint

Serve PDFs dynamically from your web application:

const express = require("express");
const app = express();

app.get("/invoice/:id/pdf", async (req, res) => {
  try {
    // Fetch invoice data from your database
    const invoice = await db.getInvoice(req.params.id);
    if (!invoice) return res.status(404).send("Invoice not found");

    const html = generateInvoiceHtml(invoice);

    const response = await fetch("https://api.snapapi.pics/v1/screenshot", {
      method: "POST",
      headers: {
        "X-Api-Key": process.env.SNAPAPI_KEY,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        html,
        format: "pdf",
        width: 794,
        height: 1123,
        responseType: "image",
      }),
    });

    if (!response.ok) throw new Error("PDF generation failed");

    const buffer = Buffer.from(await response.arrayBuffer());

    res.setHeader("Content-Type", "application/pdf");
    res.setHeader(
      "Content-Disposition",
      `attachment; filename="${invoice.number}.pdf"`
    );
    res.send(buffer);
  } catch (err) {
    console.error("PDF generation error:", err);
    res.status(500).send("Failed to generate PDF");
  }
});

app.listen(3000);

Pro tip: Set Content-Disposition to attachment to force a download, or inline to display the PDF in the browser.

CSS Print Styles

When generating PDFs from existing web pages, you can leverage CSS print media queries for optimal output. The API renders PDFs using Chromium's print functionality, so @media print rules are respected:

/* Your site's CSS */
@media print {
  /* Hide navigation and footer */
  nav, footer, .sidebar { display: none !important; }

  /* Ensure content fills the page */
  .content { max-width: 100%; margin: 0; padding: 20px; }

  /* Avoid page breaks inside elements */
  .card, .section { break-inside: avoid; }

  /* Force page break before chapters */
  .chapter { break-before: page; }

  /* Black text for readability */
  body { color: #000 !important; background: #fff !important; }
}

Common Use Cases

Next Steps

Try SnapAPI Free

Get 200 free screenshots and PDFs per month. No credit card required.

Get Started Free →

Last updated: February 19, 2026