How to Generate PDF Contracts and Proposals Automatically
Generate branded PDF contracts, proposals, and SOWs from HTML templates — on deal close, on button click, or on a schedule. No Word, no InDesign, no manual export.
Sales teams generate proposals in Word. Legal teams edit contracts in Google Docs. Neither scales: every new deal means opening a template, filling in client details, exporting to PDF, emailing it. When you're closing 10 deals a week, that's hours of manual work.
Here's how to automate it: HTML template + your CRM data + one API call = branded PDF, ready to send or sign.
Basic contract PDF
async function generateContract(contractData) {
const html = renderContractHtml(contractData);
const res = await fetch("https://pagebolt.dev/api/v1/pdf", {
method: "POST",
headers: {
"x-api-key": process.env.PAGEBOLT_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ html }),
});
if (!res.ok) throw new Error(`PageBolt error ${res.status}`);
return Buffer.from(await res.arrayBuffer());
}
Contract HTML template
function renderContractHtml({
clientName,
clientAddress,
projectName,
projectDescription,
startDate,
endDate,
totalValue,
currency = "USD",
paymentTerms,
deliverables,
contractDate,
contractNumber,
}) {
const formatted = new Intl.NumberFormat("en-US", {
style: "currency",
currency,
}).format(totalValue);
const deliverablesList = deliverables
.map((d) => `<li>${d}</li>`)
.join("");
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
font-family: Georgia, 'Times New Roman', serif;
color: #111;
max-width: 760px;
margin: 48px auto;
padding: 0 40px;
font-size: 14px;
line-height: 1.7;
}
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
border-bottom: 2px solid #111;
padding-bottom: 24px;
margin-bottom: 32px;
}
.logo { font-size: 22px; font-weight: 700; font-family: -apple-system, sans-serif; }
.contract-meta { text-align: right; color: #666; font-size: 13px; }
h1 { font-size: 20px; margin: 0 0 24px; font-family: -apple-system, sans-serif; }
h2 { font-size: 14px; font-family: -apple-system, sans-serif; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.5px; margin: 28px 0 8px; }
.parties { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; margin-bottom: 28px; }
.party { background: #f9f9f9; padding: 16px; border-radius: 4px; }
.party-label { font-size: 11px; font-weight: 700; text-transform: uppercase; color: #999; margin-bottom: 6px; }
.highlight { background: #f5f5f5; padding: 16px 20px; border-left: 3px solid #111; margin: 16px 0; }
ul { margin: 8px 0 16px 20px; }
.signatures {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 48px;
margin-top: 64px;
}
.sig-line { border-bottom: 1px solid #999; margin-bottom: 6px; height: 40px; }
.sig-label { font-size: 12px; color: #666; }
.footer { margin-top: 48px; padding-top: 16px; border-top: 1px solid #eee;
color: #999; font-size: 11px; text-align: center; }
</style>
</head>
<body>
<div class="header">
<div class="logo">YourCompany</div>
<div class="contract-meta">
<div>Contract #${contractNumber}</div>
<div>${contractDate}</div>
</div>
</div>
<h1>Service Agreement</h1>
<div class="parties">
<div class="party">
<div class="party-label">Service Provider</div>
<strong>YourCompany Inc.</strong><br>
123 Main Street<br>
San Francisco, CA 94105
</div>
<div class="party">
<div class="party-label">Client</div>
<strong>${clientName}</strong><br>
${clientAddress}
</div>
</div>
<h2>Project</h2>
<p><strong>${projectName}</strong></p>
<p>${projectDescription}</p>
<h2>Timeline</h2>
<div class="highlight">
Start date: <strong>${startDate}</strong> ·
Completion: <strong>${endDate}</strong>
</div>
<h2>Deliverables</h2>
<ul>${deliverablesList}</ul>
<h2>Compensation</h2>
<div class="highlight">
Total value: <strong>${formatted}</strong><br>
Payment terms: ${paymentTerms}
</div>
<h2>Terms</h2>
<p>This agreement constitutes the entire understanding between the parties. Any modifications must be made in writing and signed by both parties. Either party may terminate this agreement with 30 days written notice.</p>
<div class="signatures">
<div>
<div class="sig-line"></div>
<div class="sig-label">Authorized signature — YourCompany</div>
</div>
<div>
<div class="sig-line"></div>
<div class="sig-label">Authorized signature — ${clientName}</div>
</div>
</div>
<div class="footer">
Contract #${contractNumber} · Generated ${contractDate} · Confidential
</div>
</body>
</html>`;
}
Trigger on CRM deal close (HubSpot)
// Webhook from HubSpot deal stage change
app.post("/webhooks/hubspot/deal-closed", async (req, res) => {
res.json({ ok: true });
const dealId = req.body.objectId;
const deal = await hubspot.crm.deals.basicApi.getById(dealId, [
"dealname", "amount", "closedate", "hubspot_owner_id"
]);
const contact = await getAssociatedContact(dealId);
const company = await getAssociatedCompany(dealId);
const pdf = await generateContract({
clientName: company.properties.name,
clientAddress: formatAddress(company.properties),
projectName: deal.properties.dealname,
projectDescription: deal.properties.description || "Professional services as discussed.",
startDate: new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }),
endDate: new Date(deal.properties.closedate).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }),
totalValue: parseFloat(deal.properties.amount),
paymentTerms: "50% upfront, 50% on delivery",
deliverables: (deal.properties.deliverables || "Project deliverables").split("\n").filter(Boolean),
contractDate: new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }),
contractNumber: `CONTRACT-${dealId.slice(-6).toUpperCase()}`,
});
// Email to client for signature
await sendContractEmail({ pdf, contact, deal });
});
Proposal template (pre-sales)
function renderProposalHtml({ clientName, projectName, scopeItems, investment, validUntil }) {
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: -apple-system, sans-serif; max-width: 760px; margin: 48px auto; padding: 0 40px; color: #111; }
.cover { text-align: center; padding: 80px 0; border-bottom: 1px solid #eee; margin-bottom: 48px; }
.cover h1 { font-size: 36px; margin-bottom: 8px; }
.cover .subtitle { color: #666; font-size: 18px; }
h2 { font-size: 18px; margin: 36px 0 12px; border-bottom: 1px solid #eee; padding-bottom: 8px; }
.scope-item { padding: 12px 0; border-bottom: 1px solid #f5f5f5; }
.investment-box { background: #111; color: white; padding: 32px; border-radius: 8px; text-align: center; margin: 32px 0; }
.investment-box .amount { font-size: 48px; font-weight: 800; }
.investment-box .label { opacity: 0.7; margin-top: 4px; }
.valid { color: #666; font-size: 13px; text-align: center; }
</style>
</head>
<body>
<div class="cover">
<div style="font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:#999;margin-bottom:16px">Proposal for</div>
<h1>${clientName}</h1>
<div class="subtitle">${projectName}</div>
<div style="margin-top:24px;color:#999;font-size:14px">Prepared ${new Date().toLocaleDateString("en-US",{month:"long",day:"numeric",year:"numeric"})}</div>
</div>
<h2>Scope of Work</h2>
${scopeItems.map((item) => `<div class="scope-item">✓ ${item}</div>`).join("")}
<h2>Investment</h2>
<div class="investment-box">
<div class="amount">${new Intl.NumberFormat("en-US",{style:"currency",currency:"USD"}).format(investment)}</div>
<div class="label">Total project investment</div>
</div>
<div class="valid">This proposal is valid until ${validUntil}</div>
</body>
</html>`;
}
Store in S3 + send DocuSign envelope
async function sendForSignature({ pdf, contractNumber, signerEmail, signerName }) {
// Store PDF in S3
const s3Url = await uploadToCDN(pdf, `contracts/${contractNumber}.pdf`);
// Create DocuSign envelope from the PDF
const envelope = await docusign.envelopes.createEnvelope({
emailSubject: `Please sign: Contract ${contractNumber}`,
documents: [{
documentBase64: pdf.toString("base64"),
name: `Contract ${contractNumber}.pdf`,
fileExtension: "pdf",
documentId: "1",
}],
recipients: {
signers: [{
email: signerEmail,
name: signerName,
recipientId: "1",
tabs: {
signHereTabs: [{ documentId: "1", pageNumber: "last", xPosition: "200", yPosition: "600" }],
},
}],
},
status: "sent",
});
return { envelopeId: envelope.envelopeId, s3Url };
}
One API call replaces an entire document generation pipeline. The HTML template gives you full control over layout, fonts, and brand — no Word template quirks, no InDesign license.
Try it free
100 requests/month, no credit card required. OG images, screenshots, PDFs, and video — one API.
Get API Key — Free