Use case
Procurement PDF API
Procurement teams and SaaS platforms need professional PDFs at every step of the buying cycle: purchase orders sent to vendors, RFP documents distributed to bidders, vendor quotes compiled for approval, and audit-ready records attached to ERP transactions. Generating these from HTML templates means your documents always reflect live data, match your brand, and carry a permanent URL you can attach to any workflow.
Purchase order from an HTML template (Node.js / TypeScript)
A full PO template with a company header, vendor and ship-to address blocks, a shipping terms banner, a professional line items table with SKU, description, quantity, unit price, and totals, and a three-column approval signature block. Pass store: true to receive a permanent document_url you can attach to the PO record in your ERP, include in vendor email notifications, or store for audit trail purposes.
// handlers/generate-purchase-order.ts
import Mustache from "mustache";
// templates/purchase-order.html (excerpt)
const PO_TEMPLATE_HTML = `<!DOCTYPE html>
<html>
<head>
<style>
@page {
margin: 0.75in 1in;
size: letter;
@bottom-right {
content: "Page " counter(page) " of " counter(pages);
font-family: Arial, sans-serif;
font-size: 9px;
color: #666;
}
}
body {
font-family: Arial, sans-serif;
font-size: 11pt;
color: #1a1a1a;
margin: 0;
line-height: 1.4;
}
/* Header: logo left, PO meta right */
.po-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 32px;
padding-bottom: 16px;
border-bottom: 2px solid #1a1a1a;
}
.company-name {
font-size: 20pt;
font-weight: bold;
letter-spacing: -0.02em;
}
.company-meta { font-size: 9pt; color: #555; margin-top: 4px; }
.po-meta { text-align: right; }
.po-number { font-size: 16pt; font-weight: bold; color: #1a1a1a; }
.po-label {
font-size: 8pt;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #888;
margin-bottom: 2px;
}
.po-date { font-size: 10pt; color: #555; margin-top: 6px; }
/* Vendor / Ship-to block */
.address-row {
display: flex;
gap: 48px;
margin-bottom: 28px;
}
.address-block { flex: 1; }
.address-label {
font-size: 8pt;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #888;
margin-bottom: 6px;
}
.address-content { font-size: 10pt; line-height: 1.5; }
.address-name { font-weight: bold; font-size: 11pt; }
/* Shipping terms banner */
.terms-banner {
background: #f5f5f3;
border: 1px solid #ddd;
border-radius: 3px;
padding: 10px 16px;
margin-bottom: 24px;
display: flex;
gap: 40px;
font-size: 9.5pt;
}
.terms-item { display: flex; flex-direction: column; }
.terms-key { font-size: 8pt; text-transform: uppercase; letter-spacing: 0.08em; color: #888; }
.terms-value { font-weight: bold; margin-top: 2px; }
/* Line items table */
.line-items {
width: 100%;
border-collapse: collapse;
margin-bottom: 4px;
}
.line-items thead tr {
background: #1a1a1a;
color: #fff;
}
.line-items th {
font-size: 8.5pt;
text-transform: uppercase;
letter-spacing: 0.07em;
padding: 9px 12px;
text-align: left;
font-weight: 600;
}
.line-items th.right,
.line-items td.right { text-align: right; }
.line-items tbody tr { border-bottom: 1px solid #e8e8e8; }
.line-items tbody tr:nth-child(even) { background: #fafafa; }
.line-items td {
font-size: 10pt;
padding: 10px 12px;
vertical-align: top;
}
.line-items td.sku { font-family: monospace; font-size: 9pt; color: #555; }
.line-items td.desc { max-width: 240px; }
.line-items td.desc .desc-main { font-weight: 500; }
.line-items td.desc .desc-sub { font-size: 8.5pt; color: #777; margin-top: 2px; }
/* Totals block */
.totals-wrapper {
display: flex;
justify-content: flex-end;
margin-top: 0;
margin-bottom: 32px;
}
.totals-table {
width: 280px;
border-collapse: collapse;
}
.totals-table td {
font-size: 10pt;
padding: 7px 12px;
border-bottom: 1px solid #e8e8e8;
}
.totals-table td:last-child { text-align: right; }
.totals-table .total-row td {
font-size: 12pt;
font-weight: bold;
border-top: 2px solid #1a1a1a;
border-bottom: none;
padding-top: 10px;
}
/* Approval / signature block */
.approval-section { margin-top: 40px; page-break-inside: avoid; }
.approval-title {
font-size: 9pt;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #888;
margin-bottom: 20px;
}
.approval-row { display: flex; gap: 48px; }
.approval-col { flex: 1; }
.sig-line {
border-bottom: 1px solid #1a1a1a;
height: 36px;
margin-bottom: 6px;
}
.sig-label { font-size: 9pt; color: #666; }
.sig-name { font-size: 10pt; font-weight: bold; margin-top: 3px; }
.sig-title { font-size: 9pt; color: #555; }
/* Notes */
.notes-section { margin-top: 28px; }
.notes-label {
font-size: 9pt;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #888;
margin-bottom: 8px;
}
.notes-box {
border: 1px solid #ddd;
border-radius: 3px;
padding: 12px 16px;
font-size: 10pt;
color: #444;
line-height: 1.5;
}
</style>
</head>
<body>
<div class="po-header">
<div>
<div class="company-name">{{buyerName}}</div>
<div class="company-meta">{{buyerAddress}}</div>
</div>
<div class="po-meta">
<div class="po-label">Purchase Order</div>
<div class="po-number">{{poNumber}}</div>
<div class="po-date">Date: {{poDate}}</div>
</div>
</div>
<div class="address-row">
<div class="address-block">
<div class="address-label">Vendor</div>
<div class="address-content">
<div class="address-name">{{vendorName}}</div>
{{vendorAddress}}<br />
Attn: {{vendorContact}}
</div>
</div>
<div class="address-block">
<div class="address-label">Ship To</div>
<div class="address-content">
<div class="address-name">{{shipToName}}</div>
{{shipToAddress}}
</div>
</div>
</div>
<div class="terms-banner">
<div class="terms-item">
<span class="terms-key">Payment Terms</span>
<span class="terms-value">{{paymentTerms}}</span>
</div>
<div class="terms-item">
<span class="terms-key">Shipping Method</span>
<span class="terms-value">{{shippingMethod}}</span>
</div>
<div class="terms-item">
<span class="terms-key">Required By</span>
<span class="terms-value">{{requiredByDate}}</span>
</div>
<div class="terms-item">
<span class="terms-key">Requisition No.</span>
<span class="terms-value">{{requisitionNumber}}</span>
</div>
</div>
<table class="line-items">
<thead>
<tr>
<th>#</th>
<th>SKU / Part No.</th>
<th>Description</th>
<th class="right">Qty</th>
<th class="right">Unit Price</th>
<th class="right">Line Total</th>
</tr>
</thead>
<tbody>
{{#lineItems}}
<tr>
<td>{{lineNumber}}</td>
<td class="sku">{{sku}}</td>
<td class="desc">
<div class="desc-main">{{description}}</div>
{{#notes}}<div class="desc-sub">{{notes}}</div>{{/notes}}
</td>
<td class="right">{{quantity}} {{unit}}</td>
<td class="right">{{unitPrice}}</td>
<td class="right">{{lineTotal}}</td>
</tr>
{{/lineItems}}
</tbody>
</table>
<div class="totals-wrapper">
<table class="totals-table">
<tr><td>Subtotal</td><td>{{subtotal}}</td></tr>
<tr><td>Shipping & Handling</td><td>{{shippingCost}}</td></tr>
<tr><td>Tax ({{taxRate}})</td><td>{{taxAmount}}</td></tr>
<tr class="total-row"><td>Total</td><td>{{totalAmount}}</td></tr>
</table>
</div>
{{#notes}}
<div class="notes-section">
<div class="notes-label">Notes & Special Instructions</div>
<div class="notes-box">{{notes}}</div>
</div>
{{/notes}}
<div class="approval-section">
<div class="approval-title">Authorised By</div>
<div class="approval-row">
<div class="approval-col">
<div class="sig-line"></div>
<div class="sig-label">Signature</div>
<div class="sig-name">{{approverName}}</div>
<div class="sig-title">{{approverTitle}}</div>
</div>
<div class="approval-col">
<div class="sig-line"></div>
<div class="sig-label">Date Approved</div>
<div class="sig-name">{{approvalDate}}</div>
</div>
<div class="approval-col">
<div class="sig-line"></div>
<div class="sig-label">Cost Centre</div>
<div class="sig-name">{{costCentre}}</div>
</div>
</div>
</div>
</body>
</html>`;
type LineItem = {
lineNumber: number;
sku: string;
description: string;
notes?: string;
quantity: number;
unit: string;
unitPrice: string;
lineTotal: string;
};
type PurchaseOrderData = {
poNumber: string;
requisitionNumber: string;
requiredByDate: string;
vendorName: string;
vendorAddress: string;
vendorContact: string;
shipToName: string;
shipToAddress: string;
paymentTerms: string;
shippingMethod: string;
lineItems: LineItem[];
subtotal: string;
shippingCost: string;
taxRate: string;
taxAmount: string;
totalAmount: string;
approverName: string;
approverTitle: string;
approvalDate: string;
costCentre: string;
notes?: string;
};
export async function generatePurchaseOrder(data: PurchaseOrderData): Promise<string> {
const html = Mustache.render(PO_TEMPLATE_HTML, {
buyerName: "Acme Corporation",
buyerAddress: "500 Technology Drive, Austin, TX 78701",
poDate: new Date().toLocaleDateString("en-US", {
month: "long", day: "numeric", year: "numeric",
}),
...data,
});
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: `po-${data.poNumber}.pdf`,
store: true,
options: { format: "Letter" },
}),
});
const { document_url } = await res.json();
// document_url is permanent: attach it to the PO record, email it to the vendor,
// or store it in your ERP audit trail.
return document_url;
}Auto-generate on approval via webhook (Python)
A FastAPI endpoint that receives a po.approved event from a procurement platform (Coupa, Procurify, SAP Ariba, or a custom ERP), renders the PO as a PDF, and returns the permanent document URL so your system can email it to the vendor and attach it to the approval record in one step. No polling, no scheduled jobs: the PDF is generated the moment the approval fires.
# webhooks/procurement_handler.py
# Receives po.approved events from a procurement platform (e.g. Coupa, Procurify, SAP Ariba)
# and generates a stored PDF, returning the download URL for email dispatch to the vendor.
import os
import httpx
from fastapi import FastAPI, Request
from datetime import date
app = FastAPI()
PDFPIPE_KEY = os.environ["PDFPIPE_KEY"]
PDFPIPE_URL = "https://api.pdfpipe.xyz/v1/pdf"
PO_TEMPLATE = """<!DOCTYPE html>
<html>
<head>
<style>
@page {{ margin: 0.75in 1in; size: letter; }}
body {{ font-family: Arial, sans-serif; font-size: 11pt; color: #1a1a1a; margin: 0; }}
.po-header {{
display: flex; justify-content: space-between; align-items: flex-start;
border-bottom: 2px solid #1a1a1a; padding-bottom: 16px; margin-bottom: 28px;
}}
.company-name {{ font-size: 18pt; font-weight: bold; }}
.po-number {{ font-size: 14pt; font-weight: bold; text-align: right; }}
.po-label {{ font-size: 8pt; text-transform: uppercase; letter-spacing: 0.1em; color: #888; }}
.address-row {{ display: flex; gap: 48px; margin-bottom: 24px; }}
.address-block {{ flex: 1; }}
.address-label {{ font-size: 8pt; text-transform: uppercase; letter-spacing: 0.1em; color: #888; margin-bottom: 6px; }}
.address-name {{ font-weight: bold; }}
.line-items {{ width: 100%; border-collapse: collapse; margin-bottom: 4px; }}
.line-items thead tr {{ background: #1a1a1a; color: #fff; }}
.line-items th {{ font-size: 8.5pt; text-transform: uppercase; letter-spacing: 0.07em; padding: 9px 12px; text-align: left; }}
.line-items th.right, .line-items td.right {{ text-align: right; }}
.line-items tbody tr {{ border-bottom: 1px solid #e8e8e8; }}
.line-items tbody tr:nth-child(even) {{ background: #fafafa; }}
.line-items td {{ font-size: 10pt; padding: 9px 12px; }}
.totals-wrapper {{ display: flex; justify-content: flex-end; margin-bottom: 28px; }}
.totals-table {{ width: 260px; border-collapse: collapse; }}
.totals-table td {{ font-size: 10pt; padding: 7px 12px; border-bottom: 1px solid #e8e8e8; }}
.totals-table td:last-child {{ text-align: right; }}
.totals-table .total-row td {{
font-size: 12pt; font-weight: bold;
border-top: 2px solid #1a1a1a; border-bottom: none; padding-top: 10px;
}}
.approval-section {{ margin-top: 36px; page-break-inside: avoid; }}
.approval-row {{ display: flex; gap: 48px; }}
.approval-col {{ flex: 1; }}
.sig-line {{ border-bottom: 1px solid #1a1a1a; height: 36px; margin-bottom: 6px; }}
.sig-label {{ font-size: 9pt; color: #666; }}
.sig-name {{ font-size: 10pt; font-weight: bold; margin-top: 3px; }}
</style>
</head>
<body>
<div class="po-header">
<div>
<div class="company-name">{buyer_name}</div>
<div style="font-size:9pt;color:#555;margin-top:4px;">{buyer_address}</div>
</div>
<div>
<div class="po-label">Purchase Order</div>
<div class="po-number">{po_number}</div>
<div style="font-size:9.5pt;color:#555;margin-top:4px;">Date: {po_date}</div>
</div>
</div>
<div class="address-row">
<div class="address-block">
<div class="address-label">Vendor</div>
<div class="address-name">{vendor_name}</div>
<div style="font-size:10pt;">{vendor_address}</div>
</div>
<div class="address-block">
<div class="address-label">Payment Terms</div>
<div style="font-weight:bold;font-size:10pt;margin-top:6px;">{payment_terms}</div>
</div>
</div>
<table class="line-items">
<thead>
<tr>
<th>SKU</th><th>Description</th>
<th class="right">Qty</th>
<th class="right">Unit Price</th>
<th class="right">Total</th>
</tr>
</thead>
<tbody>
{line_items_html}
</tbody>
</table>
<div class="totals-wrapper">
<table class="totals-table">
<tr><td>Subtotal</td><td>{subtotal}</td></tr>
<tr><td>Tax</td><td>{tax_amount}</td></tr>
<tr class="total-row"><td>Total</td><td>{total_amount}</td></tr>
</table>
</div>
<div class="approval-section">
<div class="approval-row">
<div class="approval-col">
<div class="sig-line"></div>
<div class="sig-label">Approved By</div>
<div class="sig-name">{approver_name}</div>
<div class="sig-label">{approver_title}</div>
</div>
<div class="approval-col">
<div class="sig-line"></div>
<div class="sig-label">Date Approved</div>
<div class="sig-name">{approval_date}</div>
</div>
</div>
</div>
</body>
</html>"""
def build_line_items_html(items: list[dict]) -> str:
rows = []
for item in items:
rows.append(
f"""<tr>
<td style="font-family:monospace;font-size:9pt;color:#555;">{item['sku']}</td>
<td>{item['description']}</td>
<td class="right">{item['quantity']} {item.get('unit', 'ea')}</td>
<td class="right">{item['unit_price']}</td>
<td class="right">{item['line_total']}</td>
</tr>"""
)
return "\n".join(rows)
@app.post("/webhooks/procurement")
async def procurement_webhook(request: Request):
payload = await request.json()
event_type = payload.get("event")
if event_type != "po.approved":
return {"status": "ignored"}
po = payload["data"]
po_number = po["po_number"]
html = PO_TEMPLATE.format(
buyer_name=po["buyer"]["name"],
buyer_address=po["buyer"]["address"],
po_number=po_number,
po_date=date.today().strftime("%B %d, %Y"),
vendor_name=po["vendor"]["name"],
vendor_address=po["vendor"]["address"],
payment_terms=po.get("payment_terms", "Net 30"),
line_items_html=build_line_items_html(po["line_items"]),
subtotal=po["subtotal"],
tax_amount=po.get("tax_amount", "$0.00"),
total_amount=po["total_amount"],
approver_name=po["approver"]["name"],
approver_title=po["approver"]["title"],
approval_date=date.today().strftime("%B %d, %Y"),
)
response = httpx.post(
PDFPIPE_URL,
headers={
"Authorization": f"Bearer {PDFPIPE_KEY}",
"Content-Type": "application/json",
},
json={
"html": html,
"filename": f"po-{po_number}.pdf",
"store": True,
"options": {"format": "Letter"},
},
timeout=30,
)
response.raise_for_status()
data = response.json()
# document_url is permanent: attach to email notification, ERP record, or audit trail
return {
"status": "ok",
"po_number": po_number,
"document_url": data["document_url"],
"document_id": data["document_id"],
}End-of-month PO summary reports for finance (Python, asyncio)
At month-end, finance teams need a consolidated PO summary per cost centre for budget reconciliation and audit. Use the batch endpoint to generate one report per cost centre in a single HTTP call rather than looping over individual requests. Each report is stored with a permanent URL for the finance team, external auditors, or your document management system.
# scripts/monthly_po_summary.py
# End-of-month: generate a PO summary report per cost centre using the batch endpoint.
# Each report covers all POs approved in the billing month and is stored with a
# permanent URL for the finance team and auditors.
import os
import asyncio
import httpx
from datetime import date, timedelta
from calendar import monthrange
PDFPIPE_KEY = os.environ["PDFPIPE_KEY"]
BATCH_URL = "https://api.pdfpipe.xyz/v1/pdf/batch"
SUMMARY_TEMPLATE = """<!DOCTYPE html>
<html>
<head>
<style>
@page {{
margin: 0.75in 1in;
size: letter;
@bottom-right {{
content: "Page " counter(page) " of " counter(pages);
font-family: Arial, sans-serif; font-size: 9px; color: #666;
}}
}}
body {{ font-family: Arial, sans-serif; font-size: 10.5pt; color: #1a1a1a; margin: 0; }}
.report-header {{
border-bottom: 2px solid #1a1a1a; padding-bottom: 16px; margin-bottom: 28px;
display: flex; justify-content: space-between; align-items: flex-end;
}}
.report-title {{ font-size: 16pt; font-weight: bold; }}
.report-sub {{ font-size: 10pt; color: #555; margin-top: 4px; }}
.report-meta {{ text-align: right; font-size: 9pt; color: #666; line-height: 1.7; }}
.summary-table {{ width: 100%; border-collapse: collapse; margin-bottom: 6px; }}
.summary-table thead tr {{ background: #1a1a1a; color: #fff; }}
.summary-table th {{
font-size: 8.5pt; text-transform: uppercase; letter-spacing: 0.07em;
padding: 9px 12px; text-align: left;
}}
.summary-table th.right, .summary-table td.right {{ text-align: right; }}
.summary-table tbody tr {{ border-bottom: 1px solid #e8e8e8; }}
.summary-table tbody tr:nth-child(even) {{ background: #fafafa; }}
.summary-table td {{ font-size: 10pt; padding: 9px 12px; }}
.summary-table td.po-num {{ font-family: monospace; font-size: 9pt; color: #555; }}
.summary-table td.status-approved {{
color: #1a6e2e; font-weight: bold; font-size: 9pt;
}}
.totals-footer {{
display: flex; justify-content: flex-end; margin-top: 4px;
}}
.totals-footer table {{ width: 260px; border-collapse: collapse; }}
.totals-footer td {{ font-size: 10pt; padding: 7px 12px; border-bottom: 1px solid #e8e8e8; }}
.totals-footer td:last-child {{ text-align: right; }}
.totals-footer .grand-total td {{
font-size: 12pt; font-weight: bold;
border-top: 2px solid #1a1a1a; border-bottom: none; padding-top: 10px;
}}
</style>
</head>
<body>
<div class="report-header">
<div>
<div class="report-title">Purchase Order Summary</div>
<div class="report-sub">Cost Centre: {cost_centre_name} • {period_label}</div>
</div>
<div class="report-meta">
Generated: {generated_date}<br />
Total POs: {total_pos}<br />
Prepared by: Finance Operations
</div>
</div>
<table class="summary-table">
<thead>
<tr>
<th>PO Number</th>
<th>Vendor</th>
<th>Description</th>
<th>Approved By</th>
<th>Approval Date</th>
<th class="right">Amount</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{rows_html}
</tbody>
</table>
<div class="totals-footer">
<table>
<tr><td>Total Approved</td><td>{period_total}</td></tr>
<tr><td>Open / Pending Delivery</td><td>{open_total}</td></tr>
<tr class="grand-total"><td>Grand Total</td><td>{grand_total}</td></tr>
</table>
</div>
</body>
</html>"""
def build_rows_html(pos: list[dict]) -> str:
rows = []
for po in pos:
rows.append(
f"""<tr>
<td class="po-num">{po['po_number']}</td>
<td>{po['vendor_name']}</td>
<td>{po['description']}</td>
<td>{po['approver_name']}</td>
<td>{po['approval_date']}</td>
<td class="right">{po['total_amount']}</td>
<td class="status-approved">Approved</td>
</tr>"""
)
return "\n".join(rows)
def build_summary_html(cost_centre: dict, period_label: str) -> str:
return SUMMARY_TEMPLATE.format(
cost_centre_name=cost_centre["name"],
period_label=period_label,
generated_date=date.today().strftime("%B %d, %Y"),
total_pos=len(cost_centre["pos"]),
rows_html=build_rows_html(cost_centre["pos"]),
period_total=cost_centre["period_total"],
open_total=cost_centre["open_total"],
grand_total=cost_centre["grand_total"],
)
async def generate_monthly_summaries(cost_centres: list[dict]) -> list[dict]:
"""
Generate one summary report per cost centre in a single batch call.
Up to 50 documents per call. For larger organisations, chunk into groups of 50.
"""
today = date.today()
first_of_month = today.replace(day=1)
last_month_end = first_of_month - timedelta(days=1)
period_label = last_month_end.strftime("%B %Y")
documents = [
{
"html": build_summary_html(cc, period_label),
"filename": f"po-summary-{cc['id']}-{last_month_end.strftime('%Y-%m')}.pdf",
"store": True,
"options": {"format": "Letter"},
}
for cc in cost_centres
]
async with httpx.AsyncClient(timeout=120) as client:
response = await client.post(
BATCH_URL,
headers={
"Authorization": f"Bearer {PDFPIPE_KEY}",
"Content-Type": "application/json",
},
json={"documents": documents},
)
response.raise_for_status()
data = response.json()
results = []
for i, cc in enumerate(cost_centres):
results.append({
"cost_centre_id": cc["id"],
"cost_centre_name": cc["name"],
"document_url": data["results"][i]["document_url"],
"document_id": data["results"][i]["document_id"],
})
return results
if __name__ == "__main__":
cost_centres = load_cost_centres_with_monthly_pos() # returns list of cost centre dicts
results = asyncio.run(generate_monthly_summaries(cost_centres))
for r in results:
print(f"{r['cost_centre_name']}: {r['document_url']}")CSS for procurement documents
Procurement PDFs need tighter page margins than legal documents, a flex header that positions your company logo left and the PO number right, a dark header row on the line items table, zebra striping for readability across long item lists, a visually distinct totals row, and an approval signature block that never splits across pages. Use @page { @top-left { content: ... } } to add a running header with the company name on continuation pages, and suppress it on the first page with @page :first.
<!-- Procurement document CSS: professional table, header/logo block, approval signature -->
<style>
/* Page: tighter margins than legal docs, US Letter */
@page {
margin: 0.75in 1in;
size: letter;
/* Page number bottom-right */
@bottom-right {
content: "Page " counter(page) " of " counter(pages);
font-family: Arial, sans-serif;
font-size: 9px;
color: #666;
}
/* Company name top-left on continuation pages */
@top-left {
content: "Acme Corporation — Purchase Order";
font-family: Arial, sans-serif;
font-size: 9px;
color: #888;
}
}
/* Suppress running header on the first page */
@page :first {
@top-left { content: none; }
}
body {
font-family: Arial, sans-serif;
font-size: 11pt;
line-height: 1.4;
color: #1a1a1a;
margin: 0;
}
/* Dark header row for the line items table */
.line-items thead tr {
background: #1a1a1a;
color: #fff;
}
/* Zebra striping for line item rows */
.line-items tbody tr:nth-child(even) { background: #fafafa; }
.line-items tbody tr { border-bottom: 1px solid #e8e8e8; }
/* Bold totals row, separated by a heavier border */
.total-row td {
font-size: 12pt;
font-weight: bold;
border-top: 2px solid #1a1a1a;
}
/* Keep the approval block on one page */
.approval-section { page-break-inside: avoid; }
/* Signature lines */
.sig-line {
border-bottom: 1px solid #1a1a1a;
height: 36px;
margin-bottom: 6px;
}
/* Logo positioning: use a flex header so the logo sits left
and the PO number sits right regardless of content length */
.po-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
border-bottom: 2px solid #1a1a1a;
padding-bottom: 16px;
margin-bottom: 28px;
}
/* If you embed a logo image instead of text, size it here */
.company-logo {
max-height: 48px;
width: auto;
}
</style>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.