PDFPipe

Use case

Healthcare PDF API

Clinical documentation needs to be pixel-perfect, signed, and archivable. PDF generation from HTML templates lets health tech teams ship patient-facing documents without a dedicated reporting engine. EMR and EHR platforms, patient portals, lab systems, and digital health apps all generate documents on demand: when a doctor finalises a note, when a patient requests records, or when an encounter closes.

Lab report (Node.js / TypeScript)

An HTML template with a patient header (name, DOB, MRN), a results table (test name, result, reference range, and flag), and an ordering physician signature block. page-break-inside: avoid on the results table keeps rows from splitting across pages. The generated PDF is stored with store: true and the URL is written back to the lab_results table for the patient portal to serve.

TypeScript
// lab-report.ts
// Node.js / TypeScript — generate a lab report PDF and store the URL
import type { LabResult } from "./types";

const LAB_REPORT_HTML = (patient: {
  name: string;
  dob: string;
  mrn: string;
  orderedBy: string;
  collectedAt: string;
  results: LabResult[];
}) => `<!DOCTYPE html>
<html>
<head>
<style>
  body { font-family: 'Inter', sans-serif; color: #1a1a1a; margin: 0; padding: 48px 56px; }
  .header { display: flex; justify-content: space-between; border-bottom: 2px solid #1a1a1a; padding-bottom: 20px; margin-bottom: 28px; }
  .org-name { font-size: 20px; font-weight: 700; color: #d23a1d; }
  .report-meta { font-size: 12px; color: #666; text-align: right; line-height: 1.6; }
  .patient-block { background: #f9f9f9; border-left: 3px solid #d23a1d; padding: 16px 20px; margin-bottom: 28px; display: flex; gap: 48px; }
  .patient-block dt { font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; color: #999; margin-bottom: 2px; }
  .patient-block dd { font-size: 13px; font-weight: 600; color: #1a1a1a; margin: 0 0 12px; }
  h2 { font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; color: #999; margin-bottom: 12px; }
  .results-table { width: 100%; border-collapse: collapse; page-break-inside: avoid; }
  .results-table th { font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; color: #999;
    text-align: left; padding: 8px 12px; border-bottom: 1px solid #e5e5e5; background: #fafafa; }
  .results-table td { font-size: 13px; padding: 11px 12px; border-bottom: 1px solid #f0f0f0; color: #333; }
  .results-table td.value { font-weight: 600; color: #1a1a1a; }
  .flag-H { color: #d23a1d; font-weight: 700; }
  .flag-L { color: #2563eb; font-weight: 700; }
  .flag-N { color: #16a34a; }
  .sig-block { margin-top: 48px; border-top: 1px solid #e5e5e5; padding-top: 24px; display: flex; gap: 64px; }
  .sig-col { flex: 1; }
  .sig-line { border-bottom: 1px solid #1a1a1a; height: 32px; margin-bottom: 6px; }
  .sig-label { font-size: 12px; color: #999; }
  .sig-name { font-size: 13px; font-weight: 600; color: #1a1a1a; margin-top: 3px; }
  @page { margin: 0; size: A4; }
</style>
</head>
<body>
  <div class="header">
    <div>
      <div class="org-name">City General Hospital</div>
      <div style="font-size:12px;color:#666;margin-top:4px;">Laboratory Services</div>
    </div>
    <div class="report-meta">
      <strong>Lab Report</strong><br />
      Collected: ${patient.collectedAt}<br />
      MRN: ${patient.mrn}
    </div>
  </div>

  <div class="patient-block">
    <dl>
      <dt>Patient Name</dt>
      <dd>${patient.name}</dd>
      <dt>Date of Birth</dt>
      <dd>${patient.dob}</dd>
    </dl>
    <dl>
      <dt>MRN</dt>
      <dd>${patient.mrn}</dd>
      <dt>Ordering Physician</dt>
      <dd>${patient.orderedBy}</dd>
    </dl>
  </div>

  <h2>Results</h2>
  <table class="results-table">
    <thead>
      <tr>
        <th>Test</th>
        <th>Result</th>
        <th>Reference Range</th>
        <th>Flag</th>
      </tr>
    </thead>
    <tbody>
      ${patient.results
        .map(
          (r) => `
      <tr>
        <td>${r.testName}</td>
        <td class="value">${r.value} ${r.unit}</td>
        <td>${r.referenceRange}</td>
        <td class="flag-${r.flag}">${r.flag}</td>
      </tr>`
        )
        .join("")}
    </tbody>
  </table>

  <div class="sig-block">
    <div class="sig-col">
      <div class="sig-line"></div>
      <div class="sig-label">Verified by</div>
      <div class="sig-name">${patient.orderedBy}</div>
    </div>
    <div class="sig-col">
      <div class="sig-label" style="margin-top:38px;">Report generated: ${new Date().toLocaleString("en-US", { dateStyle: "long", timeStyle: "short" })}</div>
    </div>
  </div>
</body>
</html>`;

export async function generateLabReport(patient: {
  name: string;
  dob: string;
  mrn: string;
  orderedBy: string;
  collectedAt: string;
  results: LabResult[];
}): Promise<string> {
  const html = LAB_REPORT_HTML(patient);

  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: `lab-report-${patient.mrn}-${Date.now()}.pdf`,
      store: true,
      options: { format: "A4" },
    }),
  });

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

  // Persist the PDF URL alongside the lab order record
  await db.labResults.update(patient.mrn, {
    reportPdfId: document_id,
    reportPdfUrl: document_url,
  });

  return document_url;
}

Discharge summary on EHR webhook (Python)

A webhook handler for an encounter.discharged event. Builds the discharge summary HTML with the primary diagnosis, medications at discharge, follow-up instructions, and return-to-ED criteria. Calls the PDF API with store: true, writes the URL to the encounter record, and emails the patient a download link.

Python
# ehr_webhooks/encounter_discharged.py
# Triggered by an encounter.discharged event from your EHR system
import os
import httpx
from datetime import date

PDFPIPE_KEY = os.environ["PDFPIPE_KEY"]
PDFPIPE_URL = "https://api.pdfpipe.xyz/v1/pdf"

DISCHARGE_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<style>
  body {{ font-family: 'Inter', sans-serif; color: #1a1a1a; padding: 48px 56px; }}
  .header {{ border-bottom: 2px solid #1a1a1a; padding-bottom: 18px; margin-bottom: 24px; display: flex; justify-content: space-between; }}
  .org {{ font-size: 20px; font-weight: 700; color: #d23a1d; }}
  .doc-title {{ font-size: 18px; font-weight: 700; margin-bottom: 4px; }}
  .meta {{ font-size: 12px; color: #666; }}
  .patient-block {{ background: #f9f9f9; border-left: 3px solid #d23a1d; padding: 16px 20px; margin-bottom: 24px; }}
  .patient-block p {{ margin: 4px 0; font-size: 13px; color: #333; }}
  h2 {{ font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; color: #999; margin: 24px 0 8px; }}
  p, li {{ font-size: 13px; line-height: 1.75; color: #333; margin: 0 0 8px; }}
  ul {{ padding-left: 18px; margin: 0; }}
  .sig-block {{ margin-top: 48px; border-top: 1px solid #e5e5e5; padding-top: 24px; }}
  .sig-line {{ border-bottom: 1px solid #1a1a1a; height: 36px; width: 260px; }}
  .sig-label {{ font-size: 12px; color: #999; margin-top: 4px; }}
  @page {{ margin: 0; size: A4; }}
</style>
</head>
<body>
  <div class="header">
    <div class="org">City General Hospital</div>
    <div class="meta">Discharge Summary<br />Encounter: {encounter_id}<br />Date: {discharge_date}</div>
  </div>

  <div class="patient-block">
    <p><strong>Patient:</strong> {patient_name}</p>
    <p><strong>Date of Birth:</strong> {dob}</p>
    <p><strong>MRN:</strong> {mrn}</p>
    <p><strong>Admitting Physician:</strong> {physician_name}</p>
    <p><strong>Admission Date:</strong> {admission_date}</p>
    <p><strong>Discharge Date:</strong> {discharge_date}</p>
  </div>

  <h2>Primary Diagnosis</h2>
  <p>{diagnosis}</p>

  <h2>Medications at Discharge</h2>
  <ul>
    {medications_html}
  </ul>

  <h2>Follow-up Instructions</h2>
  <p>{followup_instructions}</p>

  <h2>Return to ED If</h2>
  <p>{return_criteria}</p>

  <div class="sig-block">
    <div class="sig-line"></div>
    <div class="sig-label">Attending Physician: {physician_name}</div>
    <div class="sig-label">Date: {discharge_date}</div>
  </div>
</body>
</html>
"""

def handle_encounter_discharged(event: dict) -> None:
    """
    Webhook handler for encounter.discharged events.
    Generates a discharge summary PDF, stores the URL, and emails the patient.
    """
    enc = event["encounter"]
    medications_html = "".join(
        f"<li>{m['name']} {m['dose']} — {m['instructions']}</li>"
        for m in enc["medications"]
    )

    html = DISCHARGE_TEMPLATE.format(
        encounter_id=enc["id"],
        patient_name=enc["patient_name"],
        dob=enc["patient_dob"],
        mrn=enc["mrn"],
        physician_name=enc["attending_physician"],
        admission_date=enc["admission_date"],
        discharge_date=date.today().strftime("%B %d, %Y"),
        diagnosis=enc["primary_diagnosis"],
        medications_html=medications_html,
        followup_instructions=enc["followup_instructions"],
        return_criteria=enc["return_criteria"],
    )

    response = httpx.post(
        PDFPIPE_URL,
        headers={
            "Authorization": f"Bearer {PDFPIPE_KEY}",
            "Content-Type": "application/json",
        },
        json={
            "html": html,
            "filename": f"discharge-summary-{enc['mrn']}-{enc['id']}.pdf",
            "store": True,
            "options": {"format": "A4"},
        },
        timeout=30,
    )
    response.raise_for_status()
    data = response.json()

    # Store the PDF URL on the encounter record
    db.encounters.update(enc["id"], {
        "discharge_summary_pdf_url": data["document_url"],
        "discharge_summary_pdf_id": data["document_id"],
    })

    # Email the patient their discharge summary
    send_email(
        to=enc["patient_email"],
        subject="Your discharge summary from City General Hospital",
        body=(
            f"Dear {enc['patient_name']},\n\n"
            "Your discharge summary is ready. You can download it at the link below.\n\n"
            f"Download: {data['document_url']}\n\n"
            "If you have any questions, please contact your care team."
        ),
    )

Referral letter (TypeScript, REST endpoint)

A short single-page referral letter with the referring physician details, patient information, reason for referral, and a clinical summary. Generated from a PUT to the referral endpoint in the health app when a clinician marks the referral as finalised. The urgency tag (Routine, Urgent, or Emergency) is rendered inline with colour coding that prints correctly in greyscale as well.

TypeScript
// routes/referrals/[referralId]/PUT.ts
// REST endpoint: PUT /referrals/:referralId
// Generates a referral letter PDF when a clinician finalises a referral
import type { Request, Response } from "express";

const REFERRAL_HTML = (data: {
  referringPhysician: string;
  referringClinic: string;
  referringPhone: string;
  patientName: string;
  patientDob: string;
  patientMrn: string;
  specialistName: string;
  specialistClinic: string;
  reasonForReferral: string;
  clinicalSummary: string;
  urgency: "Routine" | "Urgent" | "Emergency";
  letterDate: string;
}) => `<!DOCTYPE html>
<html>
<head>
<style>
  body { font-family: 'Inter', sans-serif; color: #1a1a1a; padding: 48px 56px; }
  .header { display: flex; justify-content: space-between; border-bottom: 2px solid #1a1a1a; padding-bottom: 18px; margin-bottom: 28px; }
  .org { font-size: 18px; font-weight: 700; color: #d23a1d; }
  .meta { font-size: 12px; color: #666; text-align: right; line-height: 1.65; }
  .urgency-tag { display: inline-block; padding: 3px 10px; border-radius: 3px; font-size: 11px;
    font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em;
    background: ${data.urgency === "Routine" ? "#f0fdf4" : data.urgency === "Urgent" ? "#fef9c3" : "#fef2f2"};
    color: ${data.urgency === "Routine" ? "#16a34a" : data.urgency === "Urgent" ? "#854d0e" : "#d23a1d"}; }
  p { font-size: 13px; line-height: 1.75; color: #333; margin: 0 0 14px; }
  .section-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; color: #999; margin: 20px 0 4px; }
  .sig-block { margin-top: 48px; }
  .sig-line { border-bottom: 1px solid #1a1a1a; height: 36px; width: 260px; }
  .sig-label { font-size: 12px; color: #999; margin-top: 4px; }
  @page { margin: 0; size: A4; }
</style>
</head>
<body>
  <div class="header">
    <div>
      <div class="org">${data.referringClinic}</div>
      <div style="font-size:12px;color:#666;margin-top:4px;">${data.referringPhone}</div>
    </div>
    <div class="meta">
      <strong>Referral Letter</strong><br />
      Date: ${data.letterDate}<br />
      <span class="urgency-tag">${data.urgency}</span>
    </div>
  </div>

  <p><strong>To:</strong> ${data.specialistName}<br />${data.specialistClinic}</p>

  <p>Dear ${data.specialistName},</p>

  <p>
    I am referring <strong>${data.patientName}</strong> (DOB: ${data.patientDob}, MRN: ${data.patientMrn})
    to your service for the following reason:
  </p>

  <div class="section-label">Reason for Referral</div>
  <p>${data.reasonForReferral}</p>

  <div class="section-label">Clinical Summary</div>
  <p>${data.clinicalSummary}</p>

  <p>Please do not hesitate to contact me if you require additional information.</p>

  <div class="sig-block">
    <div class="sig-line"></div>
    <div class="sig-label">${data.referringPhysician}</div>
    <div class="sig-label">${data.referringClinic}</div>
    <div class="sig-label">Date: ${data.letterDate}</div>
  </div>
</body>
</html>`;

export async function PUT(req: Request, res: Response): Promise<void> {
  const { referralId } = req.params;
  const referral = await db.referrals.findById(referralId);
  if (!referral) { res.status(404).json({ error: "Referral not found" }); return; }

  const html = REFERRAL_HTML({
    referringPhysician: referral.referringPhysician,
    referringClinic: referral.clinic,
    referringPhone: referral.clinicPhone,
    patientName: referral.patientName,
    patientDob: referral.patientDob,
    patientMrn: referral.mrn,
    specialistName: referral.specialistName,
    specialistClinic: referral.specialistClinic,
    reasonForReferral: referral.reason,
    clinicalSummary: referral.clinicalSummary,
    urgency: referral.urgency,
    letterDate: new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }),
  });

  const pdfRes = 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: `referral-${referralId}.pdf`,
      store: true,
      options: { format: "A4" },
    }),
  });

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

  await db.referrals.update(referralId, {
    status: "finalised",
    letterPdfId: document_id,
    letterPdfUrl: document_url,
  });

  res.json({ document_url, document_id });
}

A note on sensitive data

PDFPipe does not store the HTML content you send. Only the generated PDF is retained when you pass store: true. The default is store: false, which keeps nothing: the PDF is streamed directly in the response and discarded immediately after.

That said, review your organisation's data handling policies before routing patient data through any third-party service. For the highest-sensitivity documents, consider rendering HTML to PDF locally with a self-hosted solution. The API is most practical for documents where the risk profile and policy review permit third-party processing.

Plans

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

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

Try it free →