PDFPipe

API reference

PDFPipe is an HTML-to-PDF API. Send a POST request to https://api.pdfpipe.xyz/v1/pdf with a JSON body containing an html string or a url, and receive an application/pdf response. No rendering engine to install, no server to operate. Free tier: 500 documents per month.

1. Get an API key

Keys look like pp_live_…. Pick a plan (the Hobby tier is free, no card required) and you get one immediately after signup. No key needed to try it on the live playground.

2. Render a PDF

POST to /v1/pdf with html (or url) and optional options. The response body is the raw application/pdf.

curl
curl -X POST https://api.pdfpipe.xyz/v1/pdf \
  -H "Authorization: Bearer pp_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"html":"<h1>Invoice #4012</h1>","options":{"format":"A4"}}' \
  --output invoice.pdf
JavaScript / Node
const res = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
  method: "POST",
  headers: {
    Authorization: "Bearer pp_live_your_key",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    html: "<h1>Invoice #4012</h1>",
    options: { format: "A4" },
  }),
});
const pdf = Buffer.from(await res.arrayBuffer());
require("fs").writeFileSync("invoice.pdf", pdf);
Python
import requests

res = requests.post(
    "https://api.pdfpipe.xyz/v1/pdf",
    headers={"Authorization": "Bearer pp_live_your_key"},
    json={"html": "<h1>Invoice #4012</h1>", "options": {"format": "A4"}},
)
with open("invoice.pdf", "wb") as f:
    f.write(res.content)

Render from a URL

curl
curl -X POST https://api.pdfpipe.xyz/v1/pdf \
  -H "Authorization: Bearer pp_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com","options":{"format":"A4"}}' \
  --output page.pdf

Endpoints

Method & pathAuthPurpose
POST /v1/pdfBearer keyRender HTML or a URL to PDF (metered)
POST /v1/pdf/batchBearer keyBatch render up to 10-500 items (plan-based); each item counts as one render; webhook notification on completion
POST /v1/pdf/mergeBearer keyMerge 2–20 PDFs (by document ID, URL, or base64) into one; Starter+ plan
GET /v1/meBearer keyInspect the key: plan, usage this period, limit
GET /v1/documentsBearer keyList stored documents for this key (paginated)
GET /v1/documents/:idBearer keyDownload a stored document by ID
GET /v1/documents/:id/metadataBearer keyGet document metadata as JSON (no download)
DELETE /v1/documents/:idBearer keyDelete a stored document immediately
POST /v1/demononePlayground render, rate-limited per IP per day
GET /healthnoneLiveness check

PDF options

OptionValuesDefault
formatA4 · A3 · A5 · Letter · Legal · TabloidA4
landscapetrue / falsefalse
marginCSS length or {top, bottom, left, right}1cm
print_backgroundtrue / falsetrue
scale0.1 to 2.01.0
page_ranges"1-3, 5"all pages
prefer_css_page_sizetrue / false (use @page CSS)false
mediaprint / screenprint
timeout_ms1000 to 6000030000
wait_untilload · domcontentloaded · networkidle0 · networkidle2networkidle0
wait_forCSS selector to wait for before capturenone
wait_msextra delay after load, up to 50000
inject_cssCSS string injected before capturenone
header_htmlHTML string rendered as running page header (.pageNumber, .totalPages, .date classes substituted)none
footer_htmlHTML string rendered as running page footer (same class substitution)none
tabular_numstrue / false: forces consistent digit widths via font-variant-numericfalse
deduplicate_imagestrue / false: merges identical image XObjects to reduce file sizefalse
pdf_atrue / false: adds PDF/A-1b XMP metadata marker (best-effort; full conformance requires tagged structure)false

Reliable by default

The render waits for web fonts and images before drawing, so text never freezes in a fallback face and pictures are never half-loaded. If a font or image does fail, the PDF is still returned and the details appear in the X-PDFPipe-Warnings response header so you can act on them without losing the document. Page-break rules like break-inside: avoid are honored under print emulation, which keeps long tables from splitting mid-row. Use inject_css to add print-specific overrides, custom @font-face rules, or forced page-break points without touching your application HTML. Because the render uses a current browser engine, modern CSS works exactly as it does in Chrome: flexbox, grid, custom properties, web fonts, and frameworks like Tailwind and Bootstrap. Inline the stylesheet or link it from a public HTTPS CDN. One caveat: responsive breakpoints evaluate at the print page width, so design for the page size rather than a screen. When something does go wrong, the error body includes a machine-readable error field (timeout, resource_load_failed, quota_exceeded, etc.) alongside the human-readable detail. A slow page returns a timeout you can raise with timeout_ms (up to 60 s), and a renderer at capacity returns a 503 you can safely retry.

Adding images

There is no upload step. You add an image by referencing it from your HTML, in one of two ways.

1. Embed it as a data URI. Base64-encode the image and inline it. Nothing is fetched, so it can never fail to load and is never blocked. Best for logos and small fixed assets. Keep it modest in size: base64 adds about a third to the byte count.

HTML
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..." width="160" />

2. Reference a hosted HTTPS URL. The renderer fetches it and waits for it to finish loading before drawing, so it is never half-loaded. The host must be public (private, internal, and localhost addresses are blocked). For a per-document image, put the URL in a template placeholder.

HTML
<img src="https://yourcdn.com/logo.png" width="160" />

<!-- per-document image, filled from your template data -->
<img src="{{logo_url}}" width="160" />

What will not load: assets behind authentication, cookies, or a private CDN (no credentials are sent), file:// and blob: sources, and relative paths like /logo.png when you send raw HTML. Raw HTML has no base URL, so use an absolute URL or a data URI; relative paths only resolve when you render a url. If an image still fails, the PDF is returned anyway and the problem is listed in the X-PDFPipe-Warnings response header.

Usage & limits

Every successful render returns your usage in response headers: X-PDFPipe-Usage, X-PDFPipe-Limit, and X-PDFPipe-Plan. When the monthly limit is reached the API returns 402. Upgrade your plan and the limit resets immediately.

Document archive

Stored documents are visible in your dashboard when signed in, and retrievable via the /v1/documents endpoints using your Bearer key. Unauthenticated visitors cannot see stored documents.

Add "store": true to any render request and the API returns JSON instead of raw bytes. An optional filename string sets the download name. The JSON body includes:

FieldValue
document_idUnique document ID
document_urlDirect download URL (authenticated)
document_expiresISO 8601 expiry timestamp
size_bytesPDF size in bytes
used / limitCurrent quota usage

The same values are also present in response headers (X-PDFPipe-Document-Id, X-PDFPipe-Document-Url, X-PDFPipe-Document-Expires) for compatibility with HTTP clients that prefer headers over JSON bodies.

Retention varies by plan: 1 day on the free tier, 30 days on Starter, 1 year on Growth, and 2 years on Scale. Compare plans. Use GET /v1/documents to list active documents, GET /v1/documents/:id/metadata to fetch JSON metadata (filename, size, expiry) without downloading the bytes, GET /v1/documents/:id to download one, and DELETE /v1/documents/:id to remove one before it expires. The list endpoint is paginated: pass limit (1-100, default 100) and before (ISO timestamp) to page through large archives. When the response includes has_more: true, pass the next_before value as the next before parameter.

JavaScript · store, list, metadata, download, delete
// 1. Render and store — store:true returns JSON, not raw bytes
const render = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
  method: "POST",
  headers: { Authorization: "Bearer pp_live_your_key", "Content-Type": "application/json" },
  body: JSON.stringify({ html: "<h1>Invoice #4012</h1>", store: true, filename: "invoice-4012.pdf" }),
});
const { document_id: docId, document_url } = await render.json();

// 2. List all stored documents
const list = await fetch("https://api.pdfpipe.xyz/v1/documents", {
  headers: { Authorization: "Bearer pp_live_your_key" },
});
const { documents } = await list.json();

// 3. Get metadata without downloading (filename, size, expiry)
const meta = await fetch(`https://api.pdfpipe.xyz/v1/documents/${docId}/metadata`, {
  headers: { Authorization: "Bearer pp_live_your_key" },
});
// { id, filename, size_bytes, created_at, expires_at, url, expired }
const info = await meta.json();

// 4. Download a document by ID
const file = await fetch(`https://api.pdfpipe.xyz/v1/documents/${docId}`, {
  headers: { Authorization: "Bearer pp_live_your_key" },
});
const pdf = Buffer.from(await file.arrayBuffer());

// 5. Delete a document
await fetch(`https://api.pdfpipe.xyz/v1/documents/${docId}`, {
  method: "DELETE",
  headers: { Authorization: "Bearer pp_live_your_key" },
}); // 204 No Content

Document archive is available on all plans. A free account is required to retrieve stored documents. Sign up free →

Batch rendering

Render multiple HTML or URL inputs in one request with POST https://api.pdfpipe.xyz/v1/pdf/batch. Every item is stored automatically and you receive a document ID and download URL for each one. Top-level options apply to all items; per-item options override them. Each item counts as one render against your monthly quota. The endpoint is available on Starter and above.

PlanMax items per call
HobbyNot available
Starter10
Growth50
Scale100
Business250
Enterprise500

Request

Pass a requests array where each item has html or url, an optional filename, and optional per-item options.

JavaScript · batch render
const res = await fetch("https://api.pdfpipe.xyz/v1/pdf/batch", {
  method: "POST",
  headers: { Authorization: "Bearer pp_live_your_key", "Content-Type": "application/json" },
  body: JSON.stringify({
    options: { format: "A4" },           // shared across all items
    requests: [
      { html: "<h1>Invoice #4012</h1>", filename: "invoice-4012.pdf" },
      { html: "<h1>Invoice #4013</h1>", filename: "invoice-4013.pdf" },
      { url: "https://example.com/report", filename: "report.pdf" },
    ],
  }),
});
const { results, usage } = await res.json();

Response

The response is always 200, even when individual items fail. Inspect each result's status field to handle partial failures.

FieldValue
results[].indexZero-based position in the request array
results[].status"ok" or "error"
results[].idDocument ID (present on success)
results[].urlAuthenticated download URL (present on success)
results[].filenameStored filename (present on success)
results[].size_bytesPDF size in bytes (present on success)
results[].expiresISO 8601 expiry timestamp (present on success)
results[].errorError message string (present on failure)
usage.renderedNumber of items that succeeded
usage.total_this_monthTotal renders used this billing period after this call
usage.limitMonthly render limit for your plan

Webhooks

Add webhook_url to any batch request and the API will POST the completed results to your endpoint before responding. Include webhook_secret and every delivery is signed using Standard Webhooks HMAC-SHA256 in the webhook-signature header (format: v1,base64…). The API call still returns the same results: the webhook is a side-channel notification, not a replacement.

JavaScript · batch with webhook
const res = await fetch("https://api.pdfpipe.xyz/v1/pdf/batch", {
  method: "POST",
  headers: { Authorization: "Bearer pp_live_your_key", "Content-Type": "application/json" },
  body: JSON.stringify({
    webhook_url: "https://yourserver.com/hooks/pdfpipe",
    webhook_secret: "whsec_your_signing_secret",
    requests: [
      { html: "<h1>Report</h1>", filename: "report.pdf" },
    ],
  }),
});
const { results, webhook_delivered } = await res.json();
// webhook_delivered: true if your endpoint returned 2xx within 8 s

// --- On your server, verify the signature (Standard Webhooks):
// const msgId = req.headers["webhook-id"];
// const msgTs = req.headers["webhook-timestamp"];
// const toSign = `${msgId}.${msgTs}.${rawBody}`;
// const expected = "v1," + hmacSha256Base64(toSign, base64Decode(secret.replace("whsec_", "")));
// if (expected !== req.headers["webhook-signature"]) throw new Error("bad sig");

The batch endpoint is not available in the no-key playground. Use your API key and call POST https://api.pdfpipe.xyz/v1/pdf/batch directly. Get a key →

AI agents (MCP)

PDFPipe ships an MCP server so an AI agent can generate a document in one tool call. Point any MCP client at pdfpipe-mcp-server with your PDFPIPE_API_KEY and the pdfpipe_generate_pdf tool becomes available.