PDF Templates for Developers:
Stop Rebuilding Your Invoice Layout
TL;DR
ConvertStack PDF Templates let you define your document layout once and generate PDFs by passing JSON data. No HTML string per request. No Chromium to manage. Pick a pre-built template for invoices, receipts, certificates, or quotes — or build your own in the visual editor.
One POST with a template_id and a data object. That's it.
1. The problem with generating PDFs per request
Every SaaS product eventually needs to generate documents. An invoice when a payment clears. A receipt after checkout. A certificate when a course is completed. A quote when a deal reaches the proposal stage.
The instinct is to build it inline: assemble an HTML string with template literals, call your PDF library, return the bytes. It works for the first document. Then the next sprint brings a new document type, a slight layout tweak, a logo change, or a new line-item field — and you are back in the string interpolation maze.
This approach has a few predictable failure modes:
- Layout logic lives in application code. A spacing change requires a deploy. A brand update means finding every place the HTML is assembled and updating each one.
- The HTML grows unchecked. What starts as twenty lines of string concatenation becomes three hundred lines of escaped quotes, conditional blocks, and
?.map()chains. - Testing is hard. You can unit-test the data, but you cannot see what the PDF actually looks like without running the whole stack.
- Consistency is manual. Two document types built six months apart will have subtly different fonts, padding, and spacing unless someone actively enforces it.
The root cause is that document layout and application data are two different concerns, and inline string building conflates them. Templates separate them.
2. What PDF templates actually do
A PDF template is a stored HTML document with variable placeholders. Instead of building the full HTML on every request, you define the layout once — with your brand colors, fonts, table structure, header, and footer — and mark the dynamic parts with variables like {{customer_name}} or {{invoice_total}}.
At render time, you pass the template ID and a JSON data payload. The system injects your data into the placeholders, renders the resulting HTML with a headless Chromium engine, and returns a PDF. The layout never changes unless you explicitly update the template.
The separation means:
- Designers can update the invoice layout without touching application code.
- The same template handles thousands of documents — each one pixel-perfect and consistent.
- Adding a new field is a data change, not a layout change.
- You can preview any template with sample data before it ever touches production.
3. How it works: define once, render many
ConvertStack templates follow a straightforward three-step flow:
Step 1 — Create or pick a template. Open the template gallery, pick a pre-built starting point (invoice, receipt, certificate, quote, report), and customize it in the visual editor. Or build from scratch. Either way, the template is saved and given a unique ID like tpl_a3f9c2e1b8d0.
Step 2 — Identify your variables. Any value that changes per document becomes a {{variable}} placeholder. A typical invoice template might use {{invoice_number}}, {{customer_name}}, {{due_date}}, and a loop over {{line_items}}.
Step 3 — Render via API. At document generation time, POST your template ID and data payload to the ConvertStack API. Get a download URL back.
// One request. One PDF. No HTML string in your application code.
const response = await fetch('https://api.convertstack.run/v1/pdf', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_live_...',
'Content-Type': 'application/json',
},
body: JSON.stringify({
template_id: 'tpl_a3f9c2e1b8d0',
data: {
invoice_number: 'INV-2026-0041',
customer_name: 'Acme Corp',
due_date: '2026-07-15',
line_items: [
{ description: 'Pro Plan — June 2026', quantity: 1, unit_price: 99.00 },
{ description: 'Overage (12 GB)', quantity: 12, unit_price: 0.50 },
],
subtotal: 105.00,
tax: 8.93,
total: 113.93,
},
}),
});
const { url } = await response.json();
// url → presigned S3 link, valid for 1 hour
That is the entire integration. No Chromium process. No browser pool. No S3 upload logic. The layout lives in the template; your code only handles the data.
4. Real-world use cases
Invoices and billing documents
The most common use case. A billing event fires — Stripe webhook, subscription renewal, manual invoice — and your backend assembles the invoice data from your database. One API call produces a branded, properly formatted PDF that gets emailed to the customer or attached to a record in your CRM. The same template handles every customer: different names, different line items, same pixel-perfect layout.
Order receipts and shipping confirmations
E-commerce platforms and marketplaces need receipts at scale. A template with order details, product thumbnails, shipping address, and a barcode means each order confirmation is a real document — not a plain-text email. Because the template is stored server-side, updating the receipt design (adding a QR code for returns, for example) is a template edit, not a code deploy.
Certificates and credentials
Course completion certificates, compliance training records, professional licenses. These documents need to look official — the right fonts, crests, signature lines, unique certificate numbers. A certificate template lets you generate hundreds of individualized credentials in a single batch job, each with the recipient's name and completion date injected at render time.
Sales quotes and proposals
CRM-triggered quote generation is where templates earn their keep. A deal moves to "Proposal Sent" in your pipeline; a webhook fires; your backend fetches the deal data and calls the API. The prospect receives a clean, branded PDF quote with itemized pricing, validity dates, and payment terms — without a sales rep touching a Word document.
Scheduled reports
Analytics dashboards, weekly digests, compliance reports. A cron job aggregates the data, passes it to a report template, and emails the PDF to stakeholders every Monday morning. The template controls exactly how charts, tables, and summaries are laid out — consistent every week regardless of who ran the job.
5. Quickstart in Node.js, Python, PHP, Ruby
All examples assume you have a template_id from the template gallery and an API key from your dashboard.
const response = await fetch('https://api.convertstack.run/v1/pdf', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.CONVERTSTACK_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
template_id: 'tpl_a3f9c2e1b8d0',
data: {
customer_name: 'Acme Corp',
invoice_number: 'INV-0041',
total: 113.93,
},
}),
});
const { url } = await response.json();
console.log('PDF ready:', url);
import os, requests
response = requests.post(
'https://api.convertstack.run/v1/pdf',
headers={
'Authorization': f'Bearer {os.environ["CONVERTSTACK_API_KEY"]}',
'Content-Type': 'application/json',
},
json={
'template_id': 'tpl_a3f9c2e1b8d0',
'data': {
'customer_name': 'Acme Corp',
'invoice_number': 'INV-0041',
'total': 113.93,
},
},
)
url = response.json()['url']
print(f'PDF ready: {url}')
$ch = curl_init('https://api.convertstack.run/v1/pdf');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $_ENV['CONVERTSTACK_API_KEY'],
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'template_id' => 'tpl_a3f9c2e1b8d0',
'data' => [
'customer_name' => 'Acme Corp',
'invoice_number' => 'INV-0041',
'total' => 113.93,
],
]),
]);
$body = json_decode(curl_exec($ch), true);
echo 'PDF ready: ' . $body['url'];
require 'net/http'
require 'json'
uri = URI('https://api.convertstack.run/v1/pdf')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = Net::HTTP::Post.new(uri)
req['Authorization'] = "Bearer #{ENV['CONVERTSTACK_API_KEY']}"
req['Content-Type'] = 'application/json'
req.body = {
template_id: 'tpl_a3f9c2e1b8d0',
data: {
customer_name: 'Acme Corp',
invoice_number: 'INV-0041',
total: 113.93,
},
}.to_json
body = JSON.parse(http.request(req).body)
puts "PDF ready: #{body['url']}"
The response url is a presigned S3 link valid for one hour. You can redirect the user directly to it, download it server-side to store in your own bucket, or attach it to an email.
6. Pre-built templates you can use today
The template gallery ships with production-ready layouts for the most common document types. Each one is built for Chromium rendering — print-safe CSS, tested page breaks, no web fonts that fail to load.
| Template | Best for |
|---|---|
| Invoice | SaaS billing, freelance, B2B |
| Receipt | E-commerce, marketplaces |
| Certificate | Courses, compliance, credentials |
| Quote | CRM-driven proposals, agencies |
| Report | Analytics, scheduled digests |
Every template is fully editable in the visual editor. Change the logo, swap the color palette, add or remove fields — no HTML knowledge required. If you need something not covered by the gallery, you can build from a blank canvas with full HTML and CSS access.
7. Templates vs. raw HTML — when to use which
ConvertStack supports both approaches. The /v1/pdf endpoint accepts either a template_id + data object, or a raw html string. Both go through the same Chromium rendering pipeline.
| Approach | Use when |
|---|---|
| Template | Document layout is stable. You generate the same document type repeatedly. Design may change independently of code. Consistency across documents matters. |
| Raw HTML | One-off document generation. The HTML is already being assembled by your application (e.g. a legacy system). Every document has a completely unique structure. |
Most production integrations start with raw HTML to prove the concept and migrate specific document types to templates once the layout stabilizes. The API is identical either way — you are just swapping html for template_id + data.
Ready to stop rebuilding your invoice layout?
Browse the template gallery, pick a starting point, and make your first API call in under five minutes.
8. FAQ
Can I use my own HTML in a template?
Yes. The visual editor exposes the raw HTML and CSS if you want full control. You can paste in your own markup, add your own classes, and reference any web fonts you self-host. Pre-built templates are just a starting point.
How do I handle repeating rows — like invoice line items?
Templates support loop syntax for arrays in your data payload. Mark the repeating block with {{#each line_items}} and {{/each}} and reference fields inside with {{description}}, {{quantity}}, etc. The renderer expands the loop at render time.
What happens if I update a template? Does it affect past PDFs?
No. PDFs are generated at request time and stored as immutable files. Updating a template only affects future renders. If you need to regenerate a past document with the new layout, make a new API call with the same data.
Can I have multiple versions of the same template?
Each save creates a new template version. You can pin a specific version by its ID, or always use the latest. This lets you roll out a layout change gradually — test the new version in staging, then update your production template_id reference.
Is there a limit on how many templates I can create?
No hard limit on the number of templates. Each API call costs one credit regardless of which template is used.
Can I pass images dynamically?
Yes. Pass an image URL as a variable (e.g. {{company_logo_url}}) and reference it in an <img src="{{company_logo_url}}"> tag in your template. The renderer fetches the image at render time. Make sure the URL is publicly accessible from the render server.