PDFPipe

Use case

Bulk PDF API

Generate invoices, reports, or certificates for all your accounts in a single call. The POST /v1/pdf/batch endpoint renders every document and returns a permanent URL for each one. No queue to manage, no per-document round-trips.

Batch request and response

Send a requests array. Each item has its ownhtml, filename, and options. Top-level options are merged with per-item options as defaults.

Request

POST /v1/pdf/batch HTTP/1.1
Authorization: Bearer pp_live_...
Content-Type: application/json

{
  "requests": [
    {
      "html": "<html><!-- invoice 1001 --></html>",
      "filename": "invoice-1001.pdf",
      "options": { "format": "A4" }
    },
    {
      "html": "<html><!-- invoice 1002 --></html>",
      "filename": "invoice-1002.pdf",
      "options": { "format": "A4" }
    }
  ],
  "options": { "format": "A4" }
}

Response

// Response: all documents stored, URLs ready
{
  "results": [
    {
      "index": 0,
      "status": "ok",
      "id": "aB3xY...",
      "url": "https://api.pdfpipe.xyz/v1/documents/aB3xY...",
      "filename": "invoice-1001.pdf",
      "size_bytes": 48210,
      "expires": "2027-06-13T00:00:00.000Z"
    },
    {
      "index": 1,
      "status": "ok",
      "id": "cD5zW...",
      "url": "https://api.pdfpipe.xyz/v1/documents/cD5zW...",
      "filename": "invoice-1002.pdf",
      "size_bytes": 51340,
      "expires": "2027-06-13T00:00:00.000Z"
    }
  ],
  "usage": {
    "rendered": 2,
    "total_this_month": 144,
    "limit": 15000
  }
}

Month-end invoice run (Node.js)

A typical pattern: chunk your customers into batches matching your plan limit, run a batch per chunk, persist the returned URL so your customer portal always has a download link without you storing the bytes.

TypeScript
// scripts/month-end-invoices.ts — run on the 1st of every month
import Mustache from "mustache";
import { readFileSync } from "fs";

const template = readFileSync("./templates/invoice.html", "utf-8");
const PDFPIPE_KEY = process.env.PDFPIPE_KEY!;
const BATCH_SIZE = 50; // Growth plan allows 50 items per batch

async function runMonthEndInvoicing() {
  const customers = await db.customers.findActive();
  console.log(`Generating ${customers.length} invoices...`);

  for (let i = 0; i < customers.length; i += BATCH_SIZE) {
    const chunk = customers.slice(i, i + BATCH_SIZE);

    const requests = chunk.map((c) => ({
      html: Mustache.render(template, invoiceData(c)),
      filename: `invoice-${c.id}-${period()}.pdf`,
      options: { format: "A4" },
    }));

    const res = await fetch("https://api.pdfpipe.xyz/v1/pdf/batch", {
      method: "POST",
      headers: { Authorization: `Bearer ${PDFPIPE_KEY}`, "Content-Type": "application/json" },
      body: JSON.stringify({ requests }),
    });

    if (!res.ok) throw new Error(`Batch error ${res.status}: ${await res.text()}`);
    const { results, usage } = await res.json();

    const failed = results.filter((r: any) => r.status === "error").length;
    if (failed > 0) console.warn(`${failed} failures in batch ${i / BATCH_SIZE + 1}`);

    // Persist document URLs for the customer portal
    await db.invoices.bulkCreate(
      results
        .filter((r: any) => r.status === "ok")
        .map((r: any, idx: number) => ({
          customer_id: chunk[idx].id,
          period: period(),
          pdf_id: r.id,
          pdf_url: r.url,
          expires_at: new Date(r.expires),
        }))
    );

    console.log(`Batch ${i / BATCH_SIZE + 1}: ${results.length - failed}/${results.length} ok — ${usage.total_this_month}/${usage.limit} used`);
  }
}

function period() { return new Date().toISOString().slice(0, 7); }

Python async batch

Use httpx with an async client. Each chunk is awaited in sequence to stay within the batch-size limit.

Python
import os, httpx, asyncio
from pathlib import Path

PDFPIPE_KEY = os.environ["PDFPIPE_KEY"]
BATCH_SIZE = 50  # match your plan limit

template = Path("templates/report.html").read_text()

async def batch_render(customers: list[dict]) -> list[dict]:
    requests = [
        {
            "html": render_template(template, c),
            "filename": f"report-{c['id']}.pdf",
            "options": {"format": "A4"},
        }
        for c in customers
    ]

    async with httpx.AsyncClient(timeout=120) as client:
        res = await client.post(
            "https://api.pdfpipe.xyz/v1/pdf/batch",
            headers={"Authorization": f"Bearer {PDFPIPE_KEY}"},
            json={"requests": requests},
        )
        res.raise_for_status()
        return res.json()["results"]

async def run_month_end():
    customers = await db.get_active_customers()
    all_results = []
    for i in range(0, len(customers), BATCH_SIZE):
        chunk = customers[i : i + BATCH_SIZE]
        results = await batch_render(chunk)
        all_results.extend(results)
        print(f"Batch {i // BATCH_SIZE + 1}: done")
    return all_results

Webhook callback when the batch completes

Pass webhook_url and an optionalwebhook_secret. The API POSTs the full results to your endpoint once all documents are rendered, signed with an HMAC-SHA-256 signature you can verify server-side.

Node.js
// Optional: get notified when the batch finishes
const res = await fetch("https://api.pdfpipe.xyz/v1/pdf/batch", {
  method: "POST",
  headers: { Authorization: `Bearer ${PDFPIPE_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    requests,
    webhook_url: "https://yourapp.example.com/webhooks/batch-complete",
    webhook_secret: process.env.BATCH_WEBHOOK_SECRET,
  }),
});

// Your webhook receives:
// POST /webhooks/batch-complete
// X-PDFPipe-Signature: sha256=...
// {
//   "event": "batch.complete",
//   "results": [...],
//   "usage": { "rendered": 50, "total_this_month": 194, "limit": 15000 }
// }

Batch limits by plan

PlanDocuments/moMax batch sizeStoragePrice
Hobby5001 dayFree
Starter3,0001030 days$19/mo
Growth15,00050365 days$49/mo
Scale50,0001002 years$149/mo
Business100,0002502 years$499/mo

Need more than 100,000 documents a month or a batch size over 250? Contact us for enterprise pricing.

Try it free →