How to generate a certificate PDF from a template
To generate a certificate PDF, build one HTML template with {{ placeholders }} for the variable fields (recipient, course, date, issuer), store it once, then send the per-recipient data as JSON to render a PDF. Docweave compiles the placeholders into the HTML, runs it through headless Chromium, and returns the finished PDF in one API call. You don't re-render or re-upload the layout for every certificate — only the data changes.
Why template + JSON is the right source for certificates
Certificates are a template-shaped problem: one fixed design (logo, border, signature block) and a handful of fields that change per person — name, course, date, issuer. You could render raw HTML per certificate, but then your layout logic is duplicated across every render call. The cleaner pattern, and one of the three source types Docweave accepts, is source.type: "template" — you store the HTML once with {{ mustache-style }} placeholders, then pass a small JSON object per render. This is the same approach that works well for invoices, receipts, and quotes — anywhere you have one design and many data rows.
1. Build the HTML certificate template
Keep it print-first: fixed page dimensions, embedded (or web-safe) fonts, and no dependence on scripts that need a live network to render correctly — see the HTML-to-PDF API glossary entry for why that matters. Here's a minimal landscape certificate:
<!doctype html>
<html>
<head>
<style>
@page { size: 297mm 210mm; margin: 0; }
body {
margin: 0;
font-family: Georgia, 'Times New Roman', serif;
width: 297mm;
height: 210mm;
box-sizing: border-box;
padding: 20mm;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
border: 12px solid #1a2b4c;
outline: 2px solid #c9a24b;
outline-offset: -18px;
}
h1 { font-size: 14px; letter-spacing: 4px; text-transform: uppercase; color: #6b7280; margin: 0; }
.recipient { font-size: 42px; color: #1a2b4c; margin: 16px 0; }
.body-text { font-size: 16px; color: #374151; max-width: 480px; line-height: 1.6; }
.course { font-weight: bold; color: #1a2b4c; }
.footer { display: flex; justify-content: space-between; width: 60%; margin-top: 48px; }
.footer div { font-size: 13px; color: #6b7280; border-top: 1px solid #9ca3af; padding-top: 6px; }
</style>
</head>
<body>
<h1>Certificate of Completion</h1>
<div class="recipient">{{ recipientName }}</div>
<div class="body-text">
has successfully completed <span class="course">{{ courseName }}</span>
on {{ issueDate }}.
</div>
<div class="footer">
<div>{{ issuerName }}</div>
<div>{{ certificateId }}</div>
</div>
</body>
</html>Note the @page size and a fixed-width body — this pins the layout to a real page instead of letting Chromium guess a viewport, which is the most common cause of certificates rendering at the wrong scale.
2. Store the template
Save the HTML above as a stored template (via the dashboard's template editor, or the template CRUD endpoint) and note the templateId it returns — something like tmpl_certificate_v1. Stored templates are scoped to your account; only your API key can render against your templateId. If you'd rather not manage stored templates at all, you can substitute the placeholders yourself and send the result as source.type: "html" instead — useful for a one-off batch, though you lose the reuse benefit.
3. Render a certificate
Send the templateId and the per-recipient data. Always include Accept: application/pdf and set an idempotencyKey so a retried request (network blip, duplicate webhook) doesn't produce a second billed render for the same certificate.
curl https://docweave.dev/api/v1/pdf \
-H "Authorization: Bearer dw_live_your_key" \
-H "Content-Type: application/json" \
-H "Accept: application/pdf" \
-d '{
"source": {
"type": "template",
"templateId": "tmpl_certificate_v1",
"data": {
"recipientName": "Alice Lovelace",
"courseName": "Advanced TypeScript",
"issueDate": "July 14, 2026",
"issuerName": "Docweave Academy",
"certificateId": "CERT-2026-0714-AL"
}
},
"idempotencyKey": "cert-2026-0714-alice-lovelace"
}' \
--output alice-lovelace-certificate.pdfFrom Node, do the same thing with fetch and write the raw bytes to disk:
const res = await fetch('https://docweave.dev/api/v1/pdf', {
method: 'POST',
headers: {
Authorization: 'Bearer dw_live_your_key',
'Content-Type': 'application/json',
Accept: 'application/pdf',
},
body: JSON.stringify({
source: {
type: 'template',
templateId: 'tmpl_certificate_v1',
data: {
recipientName: 'Alice Lovelace',
courseName: 'Advanced TypeScript',
issueDate: 'July 14, 2026',
issuerName: 'Docweave Academy',
certificateId: 'CERT-2026-0714-AL',
},
},
idempotencyKey: 'cert-2026-0714-alice-lovelace',
}),
});
if (!res.ok) throw new Error(`Render failed: ${res.status}`);
const buffer = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('alice-lovelace-certificate.pdf', buffer);Or, if you're calling Docweave from an agent rather than a backend service, the open-source MCP server does the same render in one tool call:
await generate_pdf({
source: {
type: 'template',
templateId: 'tmpl_certificate_v1',
data: {
recipientName: 'Alice Lovelace',
courseName: 'Advanced TypeScript',
issueDate: 'July 14, 2026',
issuerName: 'Docweave Academy',
certificateId: 'CERT-2026-0714-AL',
},
},
outputPath: './alice-lovelace-certificate.pdf',
});Generating a batch of certificates
For a cohort of 50 graduates, loop over your roster and issue one render per row, each with its own idempotencyKey (e.g. cert-{courseId}-{studentId}) so a re-run of the script after a partial failure doesn't double-bill or double-issue. If your roster already lives in a spreadsheet or base, it's often less code to trigger the render from there directly — see generating PDFs from Google Sheets or from Airtable — rather than exporting to CSV and writing a custom script.
Common gotchas
- Missing placeholder data renders as a literal
{{ fieldName }}in the PDF, not an error — validate your data object against the template's expected fields before sending. - Web fonts need to actually load before the page is captured; prefer a small set of system/serif fonts for certificates, or self-host the font file rather than pointing at an external CDN.
- Long names or course titles can overflow a fixed-width layout — use
overflow-wrap: break-wordand test with your longest realistic string, not just "Alice Lovelace".
For the full certificate use case, including bulk-issuance patterns and verification links, see certificate generation. General template mechanics (placeholder syntax, data typing, versioning stored templates) are covered in the main docs, and pricing for high-volume batches (a graduating class, a course platform) is on the pricing page.
FAQ
Do I need to store the template, or can I send the full HTML every time?
You can send full HTML on every request using source.type: "html", but storing it once as a template and passing only the per-recipient JSON is simpler to maintain and keeps your render payloads small — especially once you're issuing certificates in bulk.
Can I add a QR code or verification link to the certificate?
Yes — generate the QR code as a data URI (or host it and reference the URL) and embed it as an <img> in the template, with the verification URL itself passed in as a placeholder like {{ verifyUrl }}.
What page size should a certificate use?
Landscape Letter (11in x 8.5in) or A4 (297mm x 210mm) are the standard choices. Set it explicitly with @page { size: ... } in your CSS rather than relying on a default, since the default varies by renderer.
How do I avoid issuing a duplicate certificate if my script crashes mid-batch?
Set a deterministic idempotencyKey per recipient (for example cert-{courseId}-{studentId}). Re-running the same request with the same key returns the original result instead of billing and rendering again.
Related
Generate your first PDF in minutes.
Get an API key