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.
// 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.
# 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.
// 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
| Plan | Documents/mo | Storage | 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 |
Need more than 100,000 documents per month? Contact us for enterprise pricing.