PDFPipe

Use case

Contract PDF API

Generate NDAs, service agreements, and legal documents as PDFs from HTML templates. Fill in party names, clauses, and dates server-side: no PDF libraries, no layout engines, no fonts to install.

A contract template

Render your agreement in HTML. Use any template engine (Mustache, Handlebars, Jinja2, ERB) to fill in party names, clauses, and effective dates. Serif fonts, precise margins, and a professional layout all survive the render.

HTML (templates/nda.html)
<!-- templates/nda.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <style>
    @import url('https://fonts.googleapis.com/css2?family=Lora:wght@400;700&family=Inter:wght@400;500&display=swap');
    body { font-family: 'Inter', system-ui, sans-serif; font-size: 14px; line-height: 1.7;
           color: #1a1a1a; margin: 0; padding: 0; }
    .page { padding: 72px 88px; max-width: 720px; margin: 0 auto; }
    h1 { font-family: 'Lora', serif; font-size: 28px; font-weight: 700; text-align: center;
         margin: 0 0 8px; letter-spacing: -0.02em; }
    .subtitle { text-align: center; color: #555; margin-bottom: 48px; font-size: 13px; }
    h2 { font-family: 'Lora', serif; font-size: 16px; font-weight: 700; margin: 32px 0 8px; }
    p  { margin: 0 0 14px; }
    .parties { background: #f8f8f8; border-left: 3px solid #1d1812;
               padding: 20px 24px; margin: 32px 0; }
    .parties strong { display: block; font-size: 12px; text-transform: uppercase;
                      letter-spacing: 0.06em; color: #888; margin-bottom: 4px; }
    .parties span   { font-size: 15px; font-weight: 500; }
    .signatures { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; margin-top: 64px; }
    .sig-block .line { border-bottom: 1px solid #1a1a1a; margin-bottom: 8px; height: 36px; }
    .sig-block p   { font-size: 12px; color: #555; margin: 0; }
    .sig-block .name { font-size: 14px; font-weight: 600; color: #1a1a1a; margin-top: 4px; }
    .footer { margin-top: 48px; padding-top: 24px; border-top: 1px solid #ddd;
              font-size: 11px; color: #999; text-align: center; }
    @media print { .page { padding: 56px 72px; } }
  </style>
</head>
<body>
<div class="page">
  <h1>{{docTitle}}</h1>
  <p class="subtitle">Effective date: {{effectiveDate}}</p>

  <div class="parties">
    <div>
      <strong>Disclosing Party</strong>
      <span>{{disclosingParty}}</span>
    </div>
    <div style="margin-top:16px">
      <strong>Receiving Party</strong>
      <span>{{receivingParty}}</span>
    </div>
  </div>

  <h2>1. Definition of Confidential Information</h2>
  <p>{{clause1}}</p>

  <h2>2. Obligations of Receiving Party</h2>
  <p>{{clause2}}</p>

  <h2>3. Term</h2>
  <p>This Agreement shall remain in effect for a period of {{termYears}} years from the
     effective date unless terminated earlier by mutual written consent.</p>

  <h2>4. Governing Law</h2>
  <p>This Agreement shall be governed by and construed in accordance with the laws of
     {{governingLaw}}, without regard to its conflict-of-law provisions.</p>

  <div class="signatures">
    <div class="sig-block">
      <div class="line"></div>
      <p>Signature</p>
      <p class="name">{{disclosingSignatory}}</p>
      <p>{{disclosingTitle}}</p>
      <p style="margin-top:8px;color:#999">Date: {{signDate}}</p>
    </div>
    <div class="sig-block">
      <div class="line"></div>
      <p>Signature</p>
      <p class="name">{{receivingSignatory}}</p>
      <p>{{receivingTitle}}</p>
      <p style="margin-top:8px;color:#999">Date: {{signDate}}</p>
    </div>
  </div>

  <div class="footer">
    Document reference: {{docRef}} · Generated {{generatedDate}}
  </div>
</div>
</body>
</html>

Generate and stream to the browser

Render the template, post to the API, pipe the PDF bytes straight to the response. The caller gets a file download with the correct filename.

Node.js
const Mustache = require("mustache");
const fs = require("fs");

const template = fs.readFileSync("./templates/nda.html", "utf-8");

async function generateContract(data) {
  const html = Mustache.render(template, {
    docTitle: "Non-Disclosure Agreement",
    effectiveDate: data.effectiveDate,
    disclosingParty: data.disclosingParty,
    receivingParty: data.receivingParty,
    clause1: "Confidential Information means any non-public information that ...",
    clause2: "The Receiving Party agrees to hold the Disclosing Party's ...",
    termYears: 3,
    governingLaw: data.governingLaw,
    disclosingSignatory: data.disclosingSignatory,
    disclosingTitle: data.disclosingTitle,
    receivingSignatory: data.receivingSignatory,
    receivingTitle: data.receivingTitle,
    signDate: new Date(data.effectiveDate).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }),
    docRef: `NDA-${Date.now().toString(36).toUpperCase()}`,
    generatedDate: new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }),
  });

  const res = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.PDFPIPE_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ html, options: { format: "Letter", margin: "0" } }),
  });

  if (!res.ok) throw new Error(`PDF error ${res.status}`);
  return Buffer.from(await res.arrayBuffer());
}

// Express route: generate and stream to the browser
app.post("/contracts/:id/nda", async (req, res) => {
  const contract = await db.contracts.findById(req.params.id);
  const pdf = await generateContract(contract);
  res.set({
    "Content-Type": "application/pdf",
    "Content-Disposition": `attachment; filename="NDA-${contract.ref}.pdf"`,
  }).send(pdf);
});

Store and email a link

Pass store: true to store the document and get a time-limited URL back. Email it to the signatories so they can open, review, and sign without creating an account.

Node.js
// Generate and store, then email a link
const res = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.PDFPIPE_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ html, store: true, options: { format: "Letter" } }),
});

const { document_url, document_id, document_expires } = await res.json();

await mailer.send({
  to: signatories,
  subject: "Your NDA is ready to review",
  html: `<p>Please review and sign your NDA: <a href="${document_url}">Download PDF</a></p>
         <p>This link expires on ${new Date(document_expires).toLocaleDateString()}.</p>`,
});

await db.contracts.update(contractId, { pdf_id: document_id, pdf_url: document_url });

Generate a document bundle in parallel

Onboarding often means three or four documents at once. Generate them all in parallel, store each, and email a link bundle in a single round-trip.

Node.js
// Batch for an onboarding flow: generate MSA + NDA + SOW in parallel
const [msa, nda, sow] = await Promise.all([
  generateDoc("msa", customerData),
  generateDoc("nda", customerData),
  generateDoc("sow", projectData),
]);

// All three are in S3/KV, email the bundle
await emailContractBundle(customer.email, { msa, nda, sow });

Why use an API instead of a PDF library

  • Full CSS support: custom fonts, box shadows, multi-column layouts, print media queries
  • Pixel-accurate rendering: what you see in a browser is what goes into the PDF
  • No native dependencies: no headless browser to install, no memory leaks to manage
  • Automatic retries and queuing built in: no on-call pages for a stuck renderer
  • Usage alerts at 80% and 95% so you never hit a quota wall mid-onboarding

Plans

PlanDocuments/moStorage retentionPrice
Hobby5001 dayFree
Starter3,00030 days$19/mo
Growth15,000365 days$49/mo
Scale50,0002 years$149/mo
Business100,0002 years$499/mo

Try it free →