Convert Any URL to PDF with a REST API — Complete Guide
Generating PDFs is a perennial requirement across almost every type of software: invoices, reports, contracts, compliance records, documentation exports. The traditional solutions — WeasyPrint, wkhtmltopdf, iText, Prince — all require server-side installations, dependency management, and produce inconsistent output across environments.
A headless browser approach is the modern standard because it renders pages exactly as a real user sees them, with full CSS, web fonts, and JavaScript support. SnapAPI exposes this as a REST API: send a URL or HTML string, get a PDF back. No server setup, no binary installs, no rendering inconsistencies.
The Quickest Possible PDF
Generate a PDF from any URL with a single cURL command. This is useful for testing before writing any code.
curl -X POST "https://api.snapapi.pics/v1/screenshot" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://stripe.com/docs","format":"pdf","full_page":true}' \
--output stripe-docs.pdf
# Check the output
wc -c stripe-docs.pdf # Should be several hundred KB
open stripe-docs.pdf # macOS
Replace YOUR_API_KEY with your key from snapapi.pics/register. The free tier gives you 200 requests/month at no cost.
JavaScript: PDF Generation in Node.js
import { SnapAPI } from 'snapapi-js';
import fs from 'fs';
const client = new SnapAPI(process.env.SNAPAPI_KEY);
// Generate PDF from a URL
const pdf = await client.screenshot({
url: 'https://your-app.com/invoices/INV-2026-001',
format: 'pdf',
full_page: true,
viewport: { width: 1200, height: 900 }
});
fs.writeFileSync('./invoice-001.pdf', pdf);
console.log(`PDF saved: ${(pdf.length / 1024).toFixed(1)} KB`);
Serving PDFs from an Express.js Endpoint
import express from 'express';
import { SnapAPI } from 'snapapi-js';
const app = express();
const client = new SnapAPI(process.env.SNAPAPI_KEY);
// GET /api/invoice/:id/pdf
app.get('/api/invoice/:id/pdf', async (req, res) => {
const { id } = req.params;
// Your invoice URL — the rendered HTML page in your app
const invoiceUrl = `https://your-app.com/invoices/${id}?print=1`;
try {
const pdf = await client.screenshot({
url: invoiceUrl,
format: 'pdf',
full_page: true,
viewport: { width: 1200, height: 900 },
// Pass session cookie to authenticate the invoice page
cookies: [{ name: 'session', value: req.cookies.session, domain: 'your-app.com' }]
});
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="invoice-${id}.pdf"`,
'Content-Length': pdf.length
});
res.send(pdf);
} catch (err) {
console.error('PDF generation failed:', err);
res.status(500).json({ error: 'PDF generation failed' });
}
});
Python: PDF Generation
import os
from snapapi import SnapAPI
client = SnapAPI(os.environ["SNAPAPI_KEY"])
# From a URL
pdf_bytes = client.screenshot(
url="https://your-app.com/reports/monthly",
format="pdf",
full_page=True,
width=1200
)
with open("monthly_report.pdf", "wb") as f:
f.write(pdf_bytes)
print(f"PDF generated: {len(pdf_bytes) / 1024:.1f} KB")
Generating PDFs from HTML Strings
When you need complete control over the PDF layout — invoice templates, custom reports — pass raw HTML instead of a URL. This is ideal for server-side rendering where the HTML is generated dynamically.
import { SnapAPI } from 'snapapi-js';
const client = new SnapAPI(process.env.SNAPAPI_KEY);
// Generate HTML dynamically (e.g., from a template engine)
const invoiceHtml = generateInvoiceHtml({
invoiceNumber: 'INV-2026-042',
customer: { name: 'Acme Corp', email: 'billing@acme.com' },
lineItems: [
{ description: 'SnapAPI Pro Plan', amount: 7900, quantity: 1 }
],
total: 7900,
currency: 'USD'
});
const pdf = await client.screenshot({
html: invoiceHtml,
format: 'pdf',
viewport: { width: 1200, height: 900 }
});
// Send via email, store in S3, return as download...
Example Invoice HTML Template
function generateInvoiceHtml({ invoiceNumber, customer, lineItems, total, currency }) {
const subtotal = lineItems.reduce((sum, item) => sum + item.amount * item.quantity, 0);
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: Inter, sans-serif; color: #111; max-width: 800px; margin: 0 auto; padding: 48px; }
.header { display: flex; justify-content: space-between; margin-bottom: 48px; }
.brand { font-size: 24px; font-weight: 700; color: #6366f1; }
.invoice-meta { text-align: right; color: #64748b; font-size: 14px; }
table { width: 100%; border-collapse: collapse; margin: 32px 0; }
th { text-align: left; padding: 12px 16px; background: #f8fafc; border-bottom: 2px solid #e2e8f0; }
td { padding: 12px 16px; border-bottom: 1px solid #e2e8f0; }
.total-row td { font-weight: 700; font-size: 18px; border-bottom: none; }
.footer { margin-top: 48px; color: #94a3b8; font-size: 12px; }
</style>
</head>
<body>
<div class="header">
<div class="brand">YourCompany</div>
<div class="invoice-meta">
<div><strong>Invoice ${invoiceNumber}</strong></div>
<div>${new Date().toLocaleDateString()}</div>
<div>Bill to: ${customer.name}</div>
</div>
</div>
<table>
<thead><tr><th>Description</th><th>Qty</th><th>Unit Price</th><th>Total</th></tr></thead>
<tbody>
${lineItems.map(item => `
<tr>
<td>${item.description}</td>
<td>${item.quantity}</td>
<td>${(item.amount / 100).toFixed(2)} ${currency}</td>
<td>${(item.amount * item.quantity / 100).toFixed(2)} ${currency}</td>
</tr>
`).join('')}
</tbody>
<tfoot>
<tr class="total-row">
<td colspan="3">Total</td>
<td>${(total / 100).toFixed(2)} ${currency}</td>
</tr>
</tfoot>
</table>
<div class="footer">Thank you for your business.</div>
</body></html>`;
}
Common Use Cases
Invoice and Receipt Generation
The most common PDF use case for SaaS products. When a customer upgrades their plan or completes a purchase, generate a PDF invoice and email it or make it available for download. The HTML-string approach works best here because you control the template entirely and do not need to expose an internal invoice URL to an external API.
Report Generation
Analytics dashboards, weekly summaries, compliance reports — all of these are rendered HTML pages in your app that a user might want to export. The URL approach works well: add a ?print=1 parameter to your existing report page that applies print-friendly CSS, then pass the URL to the API with appropriate auth cookies.
Web Archiving and Compliance
Regulatory requirements in finance, legal, and healthcare often require preserving web content at a specific point in time. Generating PDFs of competitor price pages, terms of service, or public filings creates a timestamped, immutable record. Schedule these with a cron job.
// Archive competitor pricing pages daily
const archiveTargets = [
'https://competitor-a.com/pricing',
'https://competitor-b.com/pricing'
];
for (const url of archiveTargets) {
const pdf = await client.screenshot({
url,
format: 'pdf',
full_page: true
});
const filename = `archive/${new URL(url).hostname}-${new Date().toISOString().split('T')[0]}.pdf`;
await s3.putObject({ Bucket: 'archives', Key: filename, Body: pdf }).promise();
console.log(`Archived: ${filename}`);
}
Documentation and Contract Exports
When users need to share a page from your app with someone who does not have access — a board member, an auditor, a vendor — offering a "Download as PDF" button is a better UX than expecting them to use browser print. Implement it as a one-liner API call behind a button.
PDF Configuration Options
| Parameter | Default | Notes |
|---|---|---|
format |
"png" | Set to "pdf" for PDF output |
full_page |
false | true = capture full document height |
viewport.width |
1280 | Use 1200 for A4-friendly output |
delay |
0 | ms to wait after load (for charts/fonts) |
cookies |
[] | Array of cookie objects for auth |
Tips for High-Quality PDFs
- Use a print stylesheet. Add
@media print { ... }CSS to your pages to hide navigation, sidebars, and other screen-only elements. Pass?print=1to trigger print mode. - Wait for charts and custom fonts. D3 charts, Chart.js visualizations, and custom web fonts load asynchronously. Use
delay: 1500to give them time to render before capture. - Set viewport width to 1200px. This maps well to A4 paper at standard print DPI and gives you predictable line breaks in text-heavy documents.
- Test with
full_page: falsefirst. This captures only the visible viewport and is faster for debugging. Enablefull_page: trueonce the layout looks correct. - Cache aggressively. Generated PDFs are expensive to produce. Cache them by URL hash + timestamp and invalidate only when the underlying data changes.
delay: 1000 to wait for fonts to load before capture.
Try SnapAPI Free
200 PDF requests/month on the free tier. No credit card, no setup. Works from cURL, JavaScript, Python, Go, PHP, Swift, and Kotlin.
Get Free API Key →Frequently Asked Questions
How does the output compare to wkhtmltopdf or WeasyPrint?
Significantly better. wkhtmltopdf uses an outdated WebKit engine and struggles with modern CSS flexbox, grid, and custom fonts. WeasyPrint is Python-only and has limited CSS support. SnapAPI uses a current Chromium engine — the same one Chrome uses — so CSS and JS render identically to what users see in their browsers.
Can I generate multi-page PDFs?
Yes. Set full_page: true and the page will be split into multiple pages automatically based on the rendered document height. Use CSS page-break-before: always to force breaks at specific points in your HTML.
What is the maximum PDF file size?
There is no hard limit, but very long pages (40+ pages) may take 10-15 seconds to generate. For documents over 20 pages, consider splitting the source HTML into logical sections and concatenating the PDFs client-side with a library like pdf-lib.
Can I add headers, footers, and page numbers?
Use CSS @page rules and position: fixed elements in your HTML to create headers and footers that appear on every page. The counter(page) CSS function provides page numbers.