Use case
Certificate PDF API
Generate course completion certificates, professional credentials, and award certificates as landscape PDFs. One HTTP call, your own HTML design, 500 free per month.
Landscape A4 or Letter
Most certificate designs are landscape. Set format: 'A4' and landscape: true. US Letter sizes are also supported.
Web fonts and decorative CSS
The renderer loads @import fonts, supports CSS variables, borders, outlines, and any layout that works in a browser: gradients, shadows, all of it.
Store and email in one call
Add store: true to get back a stable URL for emailing. No extra storage setup. Retention window depends on your plan (1 year on Growth+).
Unique certificate IDs
Generate a short UUID in your app, embed it in the template, and save it to your database. Verify authenticity later without calling the API.
Batch backfill
Starter+ plans can batch up to 10-500 certificates per call. A webhook fires when all are done, so you can update your database without polling.
Zero new dependencies
No headless browser binary to install, no Docker layer to maintain. Works from any language with an HTTP client.
1. Design the certificate template
Landscape A4, double border, Playfair Display for the recipient name. No images to upload, no proprietary template editor. Just HTML and CSS.
<!-- templates/certificate.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,700;1,400&family=Inter:wght@400;500&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body { width: 297mm; height: 210mm; overflow: hidden; background: #fffdf8; }
.cert {
width: 100%; height: 100%;
border: 12px solid #1d1812;
outline: 3px solid #d23a1d;
outline-offset: -20px;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
gap: 20px; padding: 60px;
font-family: 'Inter', system-ui, sans-serif;
}
.label { font-size: 11px; letter-spacing: 0.3em; text-transform: uppercase; color: #888; }
.org { font-family: 'Playfair Display', serif; font-size: 22px; color: #1d1812; }
.certifies { font-size: 13px; color: #555; }
.recipient {
font-family: 'Playfair Display', serif; font-style: italic;
font-size: 44px; color: #1d1812; text-align: center; line-height: 1.1;
}
.course-label { font-size: 12px; color: #888; }
.course {
font-family: 'Playfair Display', serif; font-size: 24px; font-weight: 700;
color: #1d1812; text-align: center;
}
.meta { display: flex; gap: 60px; margin-top: 10px; }
.meta-item { text-align: center; }
.meta-value { font-size: 14px; font-weight: 500; color: #1d1812; }
.meta-label { font-size: 10px; color: #888; margin-top: 3px; letter-spacing: 0.2em; text-transform: uppercase; }
.divider { width: 120px; height: 1px; background: #d23a1d; }
.cert-id { font-size: 10px; color: #bbb; margin-top: 4px; }
</style>
</head>
<body>
<div class="cert">
<div class="label">Certificate of completion</div>
<div class="org">{{organizationName}}</div>
<div class="divider"></div>
<div class="certifies">This certifies that</div>
<div class="recipient">{{recipientName}}</div>
<div class="course-label">has successfully completed</div>
<div class="course">{{courseName}}</div>
<div class="meta">
<div class="meta-item">
<div class="meta-value">{{completionDate}}</div>
<div class="meta-label">Date</div>
</div>
<div class="meta-item">
<div class="meta-value">{{duration}}</div>
<div class="meta-label">Duration</div>
</div>
{{#if grade}}
<div class="meta-item">
<div class="meta-value">{{grade}}</div>
<div class="meta-label">Grade</div>
</div>
{{/if}}
</div>
<div class="cert-id">Certificate ID: {{certId}}</div>
</div>
</body>
</html>2. Issue one certificate
Generate a short UUID in your app and embed it in the template as a certificate ID. The stable URL comes back in the response.
// Generate a certificate and return a stable URL
import Handlebars from "handlebars";
import { readFileSync } from "fs";
import { randomUUID } from "crypto";
const tmpl = Handlebars.compile(readFileSync("./templates/certificate.html", "utf-8"));
interface CertData {
organizationName: string;
recipientName: string;
courseName: string;
completionDate: string;
duration: string;
grade?: string;
}
export async function issueCertificate(data: CertData): Promise<{ url: string; id: string }> {
const certId = randomUUID().slice(0, 8).toUpperCase();
const html = tmpl({ ...data, certId });
const resp = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PDFPIPE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
store: true,
filename: `cert-${certId}.pdf`,
options: {
format: "A4",
landscape: true,
margin: { top: "0", bottom: "0", left: "0", right: "0" },
print_background: true,
},
}),
});
if (!resp.ok) throw new Error(`Certificate generation failed: ${resp.status}`);
const { document_url } = await resp.json();
return { url: document_url, id: certId };
}3. Issue on course completion
Wire this to your LMS or course webhook. The certificate URL goes into the database and the email in the same request.
// Issue certificates on course completion event (e.g. LMS webhook)
app.post("/webhooks/course-completed", async (req, res) => {
const { userId, courseId } = req.body;
const [user, course] = await Promise.all([
db.users.findById(userId),
db.courses.findById(courseId),
]);
const { url, id } = await issueCertificate({
organizationName: "Acme Academy",
recipientName: `${user.firstName} ${user.lastName}`,
courseName: course.title,
completionDate: new Date().toLocaleDateString("en-US", {
month: "long", day: "numeric", year: "numeric",
}),
duration: `${course.hours} hours`,
grade: course.passingScore ? "Pass" : undefined,
});
await db.certificates.create({ userId, courseId, url, id });
await sendEmail({
to: user.email,
subject: `Your certificate for ${course.title}`,
body: `Congratulations! Download your certificate: ${url}`,
});
res.json({ certId: id, certUrl: url });
});Batch-issue for past completions
Growth+ plans can batch up to 50 certificates per call with a webhook on completion.
// Backfill certificates for all past completions
import { readFileSync } from "fs";
import Handlebars from "handlebars";
const tmpl = Handlebars.compile(readFileSync("./templates/certificate.html", "utf-8"));
const completions = await db.courseCompletions.findMany({
where: { certificate: null },
include: { user: true, course: true },
});
const batch = completions.slice(0, 50).map(({ user, course, completedAt }) => ({
html: tmpl({
organizationName: "Acme Academy",
recipientName: `${user.firstName} ${user.lastName}`,
courseName: course.title,
completionDate: completedAt.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }),
duration: `${course.hours} hours`,
certId: user.id.slice(0, 8).toUpperCase(),
}),
filename: `cert-${user.id}.pdf`,
}));
const resp = await fetch("https://api.pdfpipe.xyz/v1/pdf/batch", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.PDFPIPE_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({
options: { format: "A4", landscape: true, print_background: true },
requests: batch,
webhook_url: "https://yourapp.com/webhooks/certs-done",
}),
});Pricing
| Plan | Price | Certificates / month | Stored retention |
|---|---|---|---|
| Hobby | Free | 500 | 1 day |
| Starter | $19 | 3,000 | 30 days |
| Growth | $49 | 15,000 | 1 year |
| Scale | $149 | 50,000 | 1 year |
| Business | $499 | 100,000 | 1 year |
Start issuing certificates
500 certificates a month free. No credit card. Key issued instantly after signup.