Landscape A4. Custom fonts. Dynamic learner name. One API call.
Build a single HTML certificate template, swap in the learner name, course title, completion date, and cohort, then POST it to get a print-ready PDF. Works for LMS platforms, cohort-based courses, workshops, trainings, and online academies. Pay only when a certificate is issued — typically ₹0.10 per certificate.
<!DOCTYPE html>
<html>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@500;700&family=Great+Vibes&display=swap" rel="stylesheet" />
<style>
@page { size: A4 landscape; margin: 0; }
body { margin: 0; font-family: 'Cormorant Garamond', serif; color: #1a1a1a; }
.cert {
width: 297mm; height: 210mm;
box-sizing: border-box; padding: 24mm;
background: #fffaf0 url('https://cdn.example.com/cert-border.svg') center/cover no-repeat;
text-align: center;
display: flex; flex-direction: column; justify-content: center;
}
.eyebrow { letter-spacing: 8px; font-size: 14px; color: #b8860b; text-transform: uppercase; }
.title { font-size: 48px; font-weight: 700; margin: 8px 0 24px; }
.awarded { font-size: 16px; color: #555; }
.name { font-family: 'Great Vibes', cursive; font-size: 76px; color: #1a1a1a; margin: 12px 0 24px; }
.course { font-size: 22px; font-style: italic; max-width: 200mm; margin: 0 auto; }
.footer { display: flex; justify-content: space-between; margin-top: auto; padding-top: 40mm; font-size: 12px; }
.sig { border-top: 1px solid #999; padding-top: 6px; min-width: 200px; }
</style>
</head>
<body>
<div class="cert">
<div class="eyebrow">Certificate of Completion</div>
<div class="title">Acme Academy</div>
<div class="awarded">This is presented to</div>
<div class="name">{{learnerName}}</div>
<div class="course">for successfully completing<br/>{{courseTitle}}<br/>on {{completionDate}}</div>
<div class="footer">
<div class="sig">Instructor<br/>{{instructorName}}</div>
<div class="sig">Verify at /cert/{{certId}}</div>
</div>
</div>
</body>
</html>import requests
from jinja2 import Template
def issue_certificate(enrollment):
html = Template(open("templates/certificate.html").read()).render(
learnerName=enrollment.user.full_name,
courseTitle=enrollment.course.title,
completionDate=enrollment.completed_at.strftime("%B %d, %Y"),
instructorName=enrollment.course.instructor.name,
certId=enrollment.certificate_id,
)
resp = requests.post(
"https://api.html2dochub.com/v1/render",
headers={"X-API-Key": HTML2DOCHUB_KEY},
json={
"type": "pdf",
"html": html,
"options": {"format": "A4", "landscape": True, "print_background": True},
"tag": f"cert:{enrollment.course.slug}",
"idempotency_key": f"cert-{enrollment.certificate_id}",
},
timeout=30,
)
resp.raise_for_status()
pdf = requests.get(resp.json()["download_url"]).content
return enrollment.store_certificate_pdf(pdf)// Issue certificates for an entire cohort in parallel without blocking.
// Each render uses async mode + webhook callback so the workers stay free.
export async function issueCohort(cohortId: string) {
const enrollments = await getCompletedEnrollments(cohortId);
const apiKey = process.env.HTML2DOCHUB_API_KEY!;
await Promise.all(enrollments.map(async (e) => {
const html = renderCertificateTemplate(e);
const res = await fetch("https://api.html2dochub.com/v1/render", {
method: "POST",
headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
body: JSON.stringify({
type: "pdf",
mode: "async",
html,
options: { format: "A4", landscape: true, print_background: true },
webhook_url: `${process.env.BASE_URL}/webhooks/certificate-ready`,
tag: `cohort:${cohortId}`,
idempotency_key: `cert-${e.certificateId}`,
}),
});
const { id } = await res.json();
await db.certificate.update({
where: { id: e.certificateId },
data: { html2DochubJobId: id, status: "rendering" },
});
}));
}Pay only for pages rendered. No subscriptions. No minimum monthly fee.
Free account. No credit card required. API ready in minutes.
Get your free API key