Pay-per-use · No subscriptions

Generate course & workshop certificates from HTML

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.

Why developers choose HTML2DocHub

Landscape A4 or Letter, with full bleed borders and decorative flourishes
Custom Google Fonts (Great Vibes, Cormorant, Playfair) rendered correctly
Dynamic fields — name, course, date, cohort, instructor signature
SVG signature embedding and QR-code verification links
Idempotency keys so re-issuing never double-charges for the same learner
One-call API — no templating engine on our side, send fully-rendered HTML
Async mode for bulk cohort issuance (1,000+ certificates at once)
~₹0.10 per certificate with the per-job floor, ~₹0.08 for multi-page designs

Code Examples

Template — landscape A4 certificatehtml
<!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>
Python — issue one certificatepython
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)
Node — bulk cohort issuance (async mode)typescript
// 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" },
    });
  }));
}

Simple, transparent pricing

Pay only for pages rendered. No subscriptions. No minimum monthly fee.

1 page PDF:~₹0.10
10 page PDF:~₹0.80
100 pages/day:~₹8/day
See full pricing details

Frequently Asked Questions

Do custom fonts like Great Vibes or Playfair render correctly?+
Yes. Include the Google Fonts <link> tag in your template head. HTML2DocHub waits for document.fonts.ready before snapshotting, so web fonts render instead of falling back to system fonts. Preconnect to fonts.googleapis.com for best load time.
Can I embed a signature image or SVG?+
Yes. Use absolute HTTPS URLs or data URIs for signature images. SVG renders at full fidelity — no rasterisation, so the certificate looks sharp at any zoom level.
How do I handle re-issuing a certificate (typo in name, etc.)?+
Change your idempotency_key (e.g., append -v2) and call the endpoint again. The previous render is preserved in your job history so you have an audit trail. Without changing the key, the API returns the original render.
Can I add a QR code for verification?+
Yes. Generate a QR code image in your app (qrcode library in Python/Node) and embed it as a data URI or CDN URL in the template. Point it at a verification URL on your domain that shows the certificate details.
What about issuing 1,000 certificates for a cohort at once?+
Use async mode with a webhook callback. Fire all 1,000 renders in parallel, let our workers process them, and each completed job POSTs to your webhook endpoint with the download URL. Total time: a few minutes, not hours.

Start rendering PDFs today

Free account. No credit card required. API ready in minutes.

Get your free API key