PDFPipe

Use case

Real estate PDF API

Generate property listing flyers, lease agreements, offer letters, and inspection reports from HTML templates. Trigger generation from your CRM, MLS feed, or property management platform. Every PDF is stored and returns a permanent, shareable URL.

Property listing HTML template

A Mustache template covering the full listing flyer layout: hero photo, address header, bedroom/bathroom/sqft specs table, description, features list, agent contact card, and a disclaimer footer. Render it server-side with live MLS data and POST the HTML to the API. No PDF library to install, no font paths to configure, no headless browser to manage.

HTML template (Mustache)
<!-- templates/property-listing.html -->
<!DOCTYPE html>
<html>
<head>
<style>
  body { font-family: 'Inter', sans-serif; color: #1a1a1a; margin: 0; padding: 0; }
  .header { background: #1d1812; color: #f5efe2; padding: 32px 48px 24px; }
  .agency-name { font-size: 13px; letter-spacing: 0.1em; text-transform: uppercase; color: #f5efe2cc; margin-bottom: 8px; }
  .address { font-size: 28px; font-weight: 700; line-height: 1.2; }
  .suburb { font-size: 16px; color: #f5efe2bb; margin-top: 4px; }
  .hero-photo { width: 100%; height: 340px; object-fit: cover; display: block; }
  .specs { display: flex; gap: 0; border-bottom: 1px solid #e5e5e5; }
  .spec-cell { flex: 1; padding: 18px 24px; border-right: 1px solid #e5e5e5; text-align: center; }
  .spec-cell:last-child { border-right: none; }
  .spec-value { font-size: 24px; font-weight: 700; color: #d23a1d; }
  .spec-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: #999; margin-top: 4px; }
  .body { padding: 36px 48px; }
  .price-tag { font-size: 22px; font-weight: 700; color: #d23a1d; margin-bottom: 20px; }
  .description { font-size: 15px; line-height: 1.7; color: #333; margin-bottom: 32px; }
  .features-heading { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: #999; margin-bottom: 12px; }
  .features { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 24px; margin-bottom: 36px; }
  .feature { font-size: 14px; color: #1a1a1a; display: flex; align-items: center; gap: 8px; }
  .feature::before { content: ""; width: 6px; height: 6px; background: #d23a1d; border-radius: 50%; flex-shrink: 0; }
  .agent { display: flex; align-items: center; gap: 20px; padding: 24px; background: #f9f9f9; border-radius: 4px; margin-bottom: 36px; }
  .agent-photo { width: 64px; height: 64px; border-radius: 50%; object-fit: cover; }
  .agent-name { font-size: 16px; font-weight: 600; }
  .agent-title { font-size: 13px; color: #666; margin-top: 2px; }
  .agent-contact { font-size: 13px; color: #333; margin-top: 8px; }
  .disclaimer { font-size: 11px; color: #aaa; line-height: 1.6; border-top: 1px solid #e5e5e5; padding-top: 20px; }
  @page { margin: 0; size: A4; }
</style>
</head>
<body>
  <div class="header">
    <div class="agency-name">{{agencyName}}</div>
    <div class="address">{{streetAddress}}</div>
    <div class="suburb">{{suburb}}, {{state}} {{postcode}}</div>
  </div>

  <img class="hero-photo" src="{{heroPhotoUrl}}" alt="Property photo" />

  <div class="specs">
    <div class="spec-cell">
      <div class="spec-value">{{bedrooms}}</div>
      <div class="spec-label">Bedrooms</div>
    </div>
    <div class="spec-cell">
      <div class="spec-value">{{bathrooms}}</div>
      <div class="spec-label">Bathrooms</div>
    </div>
    <div class="spec-cell">
      <div class="spec-value">{{carSpaces}}</div>
      <div class="spec-label">Car spaces</div>
    </div>
    <div class="spec-cell">
      <div class="spec-value">{{landSqft}}</div>
      <div class="spec-label">Sq ft</div>
    </div>
    <div class="spec-cell">
      <div class="spec-value">{{propertyType}}</div>
      <div class="spec-label">Type</div>
    </div>
  </div>

  <div class="body">
    <div class="price-tag">{{displayPrice}}</div>

    <p class="description">{{description}}</p>

    <div class="features-heading">Property features</div>
    <div class="features">
      {{#features}}
      <div class="feature">{{.}}</div>
      {{/features}}
    </div>

    <div class="agent">
      <img class="agent-photo" src="{{agent.photoUrl}}" alt="{{agent.name}}" />
      <div>
        <div class="agent-name">{{agent.name}}</div>
        <div class="agent-title">{{agent.title}}</div>
        <div class="agent-contact">
          {{agent.phone}} &nbsp;&middot;&nbsp; {{agent.email}}
        </div>
      </div>
    </div>

    <div class="disclaimer">
      {{disclaimerText}}
    </div>
  </div>
</body>
</html>

Generate a listing PDF for buyers (Node.js / TypeScript)

Fetch the property record from your database, fill the template, and call the API with store: true. The returned document_url is a stable link you can share directly with buyers or embed in your CRM. Save it alongside the listing so you never re-generate the same PDF twice.

TypeScript
// jobs/generateListingPdf.ts
// Fetch a property from your DB and render a shareable listing PDF for buyers

import Mustache from "mustache";
import { readFileSync } from "fs";

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

interface Property {
  id: string;
  streetAddress: string;
  suburb: string;
  state: string;
  postcode: string;
  heroPhotoUrl: string;
  bedrooms: number;
  bathrooms: number;
  carSpaces: number;
  landSqft: number;
  propertyType: string;
  displayPrice: string;
  description: string;
  features: string[];
  agent: {
    name: string;
    title: string;
    phone: string;
    email: string;
    photoUrl: string;
  };
}

export async function generateListingPdf(propertyId: string): Promise<string> {
  // Fetch property from your database
  const property: Property = await db.properties.findById(propertyId);

  const html = Mustache.render(template, {
    agencyName: "Meridian Property Group",
    streetAddress: property.streetAddress,
    suburb: property.suburb,
    state: property.state,
    postcode: property.postcode,
    heroPhotoUrl: property.heroPhotoUrl,
    bedrooms: property.bedrooms,
    bathrooms: property.bathrooms,
    carSpaces: property.carSpaces,
    landSqft: property.landSqft.toLocaleString(),
    propertyType: property.propertyType,
    displayPrice: property.displayPrice,
    description: property.description,
    features: property.features,
    agent: property.agent,
    disclaimerText:
      "All information provided is believed to be accurate but is not guaranteed. " +
      "Buyers should conduct their own due diligence before making any purchasing decision.",
  });

  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,
      filename: `listing-${propertyId}.pdf`,
      store: true,
      options: { format: "A4" },
    }),
  });

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

  // Persist the URL so buyers can access it from their portal
  await db.listingDocuments.upsert({
    propertyId,
    pdfId: document_id,
    pdfUrl: document_url,
    expiresAt: new Date(document_expires),
    generatedAt: new Date(),
  });

  return document_url;
}

Lease agreement PDF (Python)

The same pattern works for lease agreements, offer letters, and inspection reports. Render a Jinja2 template with the lease terms, POST to the API with store: true, and persist the returned URL for tenant and landlord portals.

Python
# Generate a lease agreement PDF from a Jinja2 template
import os, requests
from jinja2 import Environment, FileSystemLoader

env = Environment(loader=FileSystemLoader("templates"))
template = env.get_template("lease-agreement.html")

html = template.render(
    landlord_name=lease.landlord_name,
    tenant_name=lease.tenant_name,
    property_address=lease.property_address,
    lease_start=lease.start_date.strftime("%B %d, %Y"),
    lease_end=lease.end_date.strftime("%B %d, %Y"),
    monthly_rent=f"${lease.monthly_rent:,.2f}",
    security_deposit=f"${lease.security_deposit:,.2f}",
    clauses=lease.clauses,
)

response = requests.post(
    "https://api.pdfpipe.xyz/v1/pdf",
    headers={
        "Authorization": f"Bearer {os.environ['PDFPIPE_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "html": html,
        "filename": f"lease-{lease.id}.pdf",
        "store": True,
        "options": {"format": "A4"},
    },
)

data = response.json()
lease.pdf_url = data["document_url"]
lease.pdf_expires_at = data["document_expires"]
db.session.commit()

Batch generation for MLS feeds

New listings arrive in bulk from MLS feeds. Rather than calling the API once per listing, use the batch endpoint to submit up to 100 documents in a single request. Run this as a nightly job to catch up on all new listings added during the day. The batch endpoint returns one result object per document in the same order as the input array, so you can zip results back to their source records in one pass.

TypeScript
// scripts/nightly-mls-listings.ts
// Run overnight via cron: generate listing PDFs for all new MLS listings

import { readFileSync } from "fs";
import Mustache from "mustache";

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

async function generateNightlyListings() {
  // Fetch all listings added to the MLS feed today without a PDF yet
  const newListings = await db.properties.findAll({
    where: { pdfUrl: null, listedAt: { gte: startOfToday() } },
  });

  console.log(`Generating PDFs for ${newListings.length} new listings...`);

  // Build the batch payload — one entry per listing
  const documents = newListings.map((property) => ({
    html: Mustache.render(template, buildTemplateData(property)),
    filename: `listing-${property.mlsId}.pdf`,
    store: true,
    options: { format: "A4" },
  }));

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

  const { results } = await res.json();

  // Persist all returned URLs
  await db.$transaction(
    results.map((result: { document_id: string; document_url: string; document_expires: string }, i: number) =>
      db.listingDocuments.upsert({
        propertyId: newListings[i].id,
        pdfId: result.document_id,
        pdfUrl: result.document_url,
        expiresAt: new Date(result.document_expires),
        generatedAt: new Date(),
      })
    )
  );

  console.log(`Done. ${results.length} listing PDFs stored.`);
}

generateNightlyListings().catch(console.error);

Plans

PlanPDFs/moStoragePrice
Hobby5001 dayFree
Starter3,00030 days$19/mo
Growth15,000365 days$49/mo
Scale50,0002 years$149/mo
Business100,0002 years$499/mo

Generating more than 100,000 documents per month? Contact us for enterprise pricing.

Try it free →