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.
<!-- 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.
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.
// 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.
// 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
| Plan | Documents/mo | Storage retention | Price |
|---|---|---|---|
| Hobby | 500 | 1 day | Free |
| Starter | 3,000 | 30 days | $19/mo |
| Growth | 15,000 | 365 days | $49/mo |
| Scale | 50,000 | 2 years | $149/mo |
| Business | 100,000 | 2 years | $499/mo |