DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

How to generate PDF contracts and proposals automatically

How to Generate PDF Contracts and Proposals Automatically

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());
}
Enter fullscreen mode Exit fullscreen mode

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> &nbsp;·&nbsp;
    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>`;
}
Enter fullscreen mode Exit fullscreen mode

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 });
});
Enter fullscreen mode Exit fullscreen mode

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>`;
}
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode

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. → Get started in 2 minutes

Top comments (0)