Skip to content

POSThttps://api.pdfpipe.xyz/v1/pdf

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: 100 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); up to 10 answer in the call, larger batches run in the background (202 + batch_id); each stored item counts as one render
GET/v1/pdf/batch/:idBearer keyStatus and per-item results of a background batch
POST/v1/pdf/mergeBearer keyMerge 2 to 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/schedulesBearer keyCreate a scheduled batch (Starter+ plan): cron or daily / weekly / monthly, in your time zone
GET/v1/schedulesBearer keyList this key's schedules with next run and last result
GET/v1/schedules/:idBearer keyRead one schedule
PATCH/v1/schedules/:idBearer keyReplace the data list, rename, pause or resume
DELETE/v1/schedules/:idBearer keyDelete a schedule
POST/v1/supportBearer keyOpen a support request, prioritised by plan
GET/v1/statusnoneLive status, 90-day uptime and incidents, checked every 5 minutes (real render probe hourly)
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 · networkidle2domcontentloaded, then waits for stylesheets, fonts and images
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 and Business. 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 (a top-level store: true is accepted but changes nothing; store: false is refused) 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, and an item that fails is not charged. The endpoint is available on Starter and above.

  • Up to 10 items: rendered during the call, a few at a time, and answered with 200 and a result for every item.
  • More than 10 items, or any batch sent with "async": true: answered at once with 202, a batch_id and a status_url. Every item is then rendered in the background. Poll GET /v1/pdf/batch/:id or pass a webhook_url to be told when it is complete. Allow roughly 2 to 4 seconds per document, two at a time, so 250 documents take about 4 to 9 minutes.
  • No item is left out: each one ends with a stored document or an error with a code. A renderer hiccup is retried in the background before an item is reported as failed.
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();

// More than 10 items (or async: true): 202, then poll.
// { batch_id: "bat_...", status: "queued", total: 120, status_url: "https://api.pdfpipe.xyz/v1/pdf/batch/bat_..." }
const batch = await (await fetch(status_url, { headers: { Authorization: "Bearer pp_live_your_key" } })).json();
// { status: "queued" | "running" | "complete", total, succeeded, failed, pending, results: [...] }

Response

A batch answered in the call returns 200, even when individual items fail. A background batch returns 202 and its results come from the status URL or the webhook, in the same shape. Inspect each result's status field to handle partial failures.

FieldValue
results[].indexZero-based position in the request array
results[].status"ok" or "error" ("pending" while a background batch is still running)
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)
results[].codeWhy it failed: e.g. invalid_request, renderer_unavailable, browser_daily_limit (the renderer's daily capacity is used up: resend after 00:00 UTC), batch_time_limit (a call with up to 10 items ran out of time before this item started: resend it or use async)
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 a batch.complete event with every result to your endpoint: before responding for a batch answered in the call, and when the last item finishes for a background batch (the webhook-id is then the batch_id). Include webhook_secret and every delivery is signed using Standard Webhooks HMAC-SHA256 in the webhook-signature header (format: v1,base64…).

Secret format: 16 to 256 characters. Preferred is the Standard Webhooks form, whsec_ followed by base64, where the HMAC key is the decoded base64. A secret without the prefix is used as base64 when it is valid base64, otherwise its UTF-8 bytes are the key. A whsec_ secret whose remainder is not base64 is refused with 400 before anything renders.

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_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw",   // whsec_ + base64
    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.slice("whsec_".length)));
// 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 →

Scheduled batches

Store a batch once and it runs on its own: month-end statements, weekly audit reports, daily summaries. Send a template (or HTML) and a data list with one object per document to POST https://api.pdfpipe.xyz/v1/schedules. Each run goes through the same pipeline as /v1/pdf/batch, so every document counts as one render, is stored for your plan's retention period, and appears in /v1/documents.

  • Timing: every (daily, weekly with day_of_week 0-6, monthly with day_of_month 1-28) plus at ("HH:MM"), or a five-field cron expression. Both are read in timezone (IANA, default UTC). At most one run an hour.
  • A run starts within 5 minutes after its scheduled time and renders in the background like a large batch, so every item in the list is rendered or reported with an error code. Until it finishes, last_status reads running and last_result.batch_id can be polled at GET /v1/pdf/batch/:id. Missed runs are not replayed.
  • data is capped at your plan's batch size (10 on Starter up to 250 on Business). filename can use the same {{field}} placeholders as the template. Every item also gets {{run.date}}, {{run.month}} and {{run.previous_month}}, on the schedule's clock. Replace the list any time with PATCH /v1/schedules/:id.
  • Data that changes between runs: instead of template_id or html, give a url pattern such as https://app.example.com/reports/{{id}}?month={{run.previous_month}}. Each run renders your live page, so the numbers are current.
  • Delivery: documents are always stored. Add webhook_url (and webhook_secret to sign it) to get a schedule.run.complete event with every document link after each run.
  • Up to 25 schedules per account. A schedule pauses itself after 5 failed runs in a row.
JavaScript · monthly statements
const res = await fetch("https://api.pdfpipe.xyz/v1/schedules", {
  method: "POST",
  headers: { Authorization: "Bearer pp_live_your_key", "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "Monthly statements",
    every: "monthly", day_of_month: 1, at: "06:00", timezone: "Europe/London",
    template_id: "tmpl_statement",          // or html: "<h1>{{name}}</h1>..."
    filename: "statement-{{account_id}}.pdf",
    data: [
      { account_id: "A-1001", name: "Acme Ltd", balance: "1,240.00" },
      { account_id: "A-1002", name: "Globex", balance: "310.50" },
    ],
    webhook_url: "https://yourserver.com/hooks/statements",
    webhook_secret: "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw",
  }),
});
const schedule = await res.json();
// { id: "sch_...", cron: "0 6 1 * *", next_run_at: "2026-10-01T05:00:00.000Z", ... }

// Later: GET /v1/schedules shows last_run_at, last_status and last_result.
// DELETE /v1/schedules/sch_... stops it.

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.