Get started

How to build a PDF certificate generator

Build a certificate generator with HTML templates, Google Fonts, and a single API call. Covers design, batch generation from CSV, watermarks, and security.

benoitded9 min read

A PDF certificate generator takes structured data (recipient name, course title, date) and an HTML template, then produces a finished PDF in a single API call. Instead of manually editing a design file for each recipient, your application generates hundreds of certificates in minutes with consistent formatting.

Certificates show up everywhere: course completions, event attendance, employee training, awards, professional accreditations. The manual approach (open a design tool, type a name, export, repeat) breaks down past ten recipients. At a hundred, it takes a full day. At a thousand, it's not feasible.

ApproachSetup timePer-certificate timeAutomation
Manual design tool (Canva, Word)Minutes per certificate3-5 min eachNone
PDF API (PDF4.dev)30 min one-time setup200-400ms eachFull
Self-hosted (Playwright + Docker)Hours200-400ms eachFull, you manage infra

How to design a certificate HTML template

A certificate template needs landscape orientation, elegant typography, a decorative border, and Handlebars variables for dynamic fields. Here is a complete template for PDF4.dev's a4-landscape preset:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: 'Inter', sans-serif;
      display: flex; align-items: center; justify-content: center;
      height: 100%; background: #fff;
    }
    .certificate {
      width: 100%; max-width: 900px; padding: 50px 60px;
      text-align: center; border: 3px double #7c3aed;
      border-radius: 4px; position: relative;
    }
    .certificate::before {
      content: ''; position: absolute; inset: 8px;
      border: 1px solid #e5e7eb; border-radius: 2px; pointer-events: none;
    }
    .label {
      font-size: 13px; text-transform: uppercase;
      letter-spacing: 4px; color: #7c3aed; margin-bottom: 8px;
    }
    .title {
      font-family: 'Playfair Display', serif; font-size: 36px;
      font-weight: 700; color: #111827; margin-bottom: 32px;
    }
    .subtitle { font-size: 14px; color: #6b7280; margin-bottom: 12px; }
    .recipient {
      font-family: 'Playfair Display', serif; font-size: 32px;
      font-weight: 600; color: #111827; border-bottom: 2px solid #7c3aed;
      display: inline-block; padding-bottom: 4px; margin-bottom: 24px;
    }
    .course { font-size: 18px; color: #374151; margin-bottom: 40px; line-height: 1.5; }
    .details {
      display: flex; justify-content: space-between; align-items: flex-end;
      margin-top: 40px; padding-top: 24px; border-top: 1px solid #e5e7eb;
    }
    .detail-label {
      font-size: 11px; text-transform: uppercase;
      letter-spacing: 2px; color: #9ca3af; margin-bottom: 4px;
    }
    .detail-value { font-size: 14px; color: #111827; font-weight: 500; }
    .signature-line { width: 180px; border-bottom: 1px solid #374151; margin-bottom: 4px; }
    .cert-id { font-size: 10px; color: #9ca3af; margin-top: 24px; letter-spacing: 1px; }
  </style>
</head>
<body>
  <div class="certificate">
    <div class="label">Certificate of Completion</div>
    <div class="title">{{certificate_title}}</div>
    <div class="subtitle">This is to certify that</div>
    <div class="recipient">{{recipient_name}}</div>
    <div class="course">
      has successfully completed<br>
      <strong>{{course_name}}</strong>
      {{#if hours}}<br>Duration: {{hours}} hours{{/if}}
    </div>
    <div class="details">
      <div>
        <div class="detail-label">Date</div>
        <div class="detail-value">{{formatDate date "long"}}</div>
      </div>
      <div style="text-align: center;">
        <div class="signature-line"></div>
        <div class="detail-label">{{signatory_title}}</div>
        <div class="detail-value">{{signatory_name}}</div>
      </div>
      <div style="text-align: right;">
        <div class="detail-label">Organization</div>
        <div class="detail-value">{{organization}}</div>
      </div>
    </div>
    <div class="cert-id">ID: {{certificate_id}}</div>
  </div>
</body>
</html>

The template uses Playfair Display for headings and Inter for body text. The double border with an inner ::before pseudo-element creates the classic certificate frame without image assets. All dynamic fields ({{recipient_name}}, {{course_name}}, {{date}}) are Handlebars variables replaced at render time. {{formatDate date "long"}} formats the date as "April 15, 2026" using the built-in helper.

How to create the template on PDF4.dev

The fastest path: sign in at pdf4.dev/dashboard, click "New template," paste the HTML, set the preset to A4 Landscape, and add the Google Fonts URL https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&display=swap in the Format panel.

For CI/CD pipelines where templates are version-controlled, create via API:

const response = await fetch('https://pdf4.dev/api/v1/templates', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.PDF4_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Course Certificate',
    html: certificateHtml,
    pdf_format: {
      preset: 'a4-landscape',
      margins: { top: '10mm', bottom: '10mm', left: '10mm', right: '10mm' },
      google_fonts_url:
        'https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&display=swap',
    },
    sample_data: {
      certificate_title: 'Professional Development',
      recipient_name: 'Jane Smith',
      course_name: 'Advanced TypeScript Patterns',
      hours: 40,
      date: '2026-04-15',
      signatory_name: 'Dr. Sarah Chen',
      signatory_title: 'Director of Education',
      organization: 'Tech Academy',
      certificate_id: 'CERT-2026-00042',
    },
  }),
});
 
const template = await response.json();
console.log('Template ID:', template.id);

How to generate a certificate via API

With the template created, generating a certificate is a single POST request with the recipient data.

const response = await fetch('https://pdf4.dev/api/v1/render', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.PDF4_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    template_id: 'course-certificate',
    data: {
      certificate_title: 'Professional Development',
      recipient_name: 'Jane Smith',
      course_name: 'Advanced TypeScript Patterns',
      hours: 40,
      date: '2026-04-15',
      signatory_name: 'Dr. Sarah Chen',
      signatory_title: 'Director of Education',
      organization: 'Tech Academy',
      certificate_id: 'CERT-2026-00042',
    },
  }),
});
 
const pdf = Buffer.from(await response.arrayBuffer());
fs.writeFileSync('certificate-jane-smith.pdf', pdf);

The render takes 200-400ms. Because PDF4.dev renders with Chromium, the output matches what you see in a browser print preview: exact fonts, borders, and layout.

How to batch generate certificates from CSV

Batch processing is where an API-based generator pays off. A CSV with 200 attendees produces 200 PDFs in under 30 seconds.

recipient_name,course_name,hours,date,certificate_id
Jane Smith,Advanced TypeScript Patterns,40,2026-04-15,CERT-2026-00042
Alex Johnson,React Performance Workshop,16,2026-04-15,CERT-2026-00043
Maria Garcia,Node.js Security Fundamentals,24,2026-04-15,CERT-2026-00044
import { parse } from 'csv-parse/sync';
import * as fs from 'fs';
import JSZip from 'jszip';
 
async function generateCertificates(csvPath: string) {
  const csv = fs.readFileSync(csvPath, 'utf-8');
  const attendees = parse(csv, { columns: true, skip_empty_lines: true });
  const zip = new JSZip();
  const concurrency = 10;
 
  for (let i = 0; i < attendees.length; i += concurrency) {
    const batch = attendees.slice(i, i + concurrency);
    const results = await Promise.allSettled(
      batch.map(async (attendee) => {
        const response = await fetch('https://pdf4.dev/api/v1/render', {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${process.env.PDF4_API_KEY}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            template_id: 'course-certificate',
            data: {
              ...attendee,
              hours: parseInt(attendee.hours, 10),
              certificate_title: 'Professional Development',
              signatory_name: 'Dr. Sarah Chen',
              signatory_title: 'Director of Education',
              organization: 'Tech Academy',
            },
          }),
        });
        if (!response.ok) throw new Error(`${attendee.recipient_name}: failed`);
        return {
          name: attendee.recipient_name,
          pdf: Buffer.from(await response.arrayBuffer()),
        };
      })
    );
 
    for (const r of results) {
      if (r.status === 'fulfilled') {
        const filename = `${r.value.name.replace(/\s+/g, '-').toLowerCase()}.pdf`;
        zip.file(filename, r.value.pdf);
      } else {
        console.error('Failed:', r.reason);
      }
    }
  }
 
  const zipBuffer = await zip.generateAsync({ type: 'nodebuffer' });
  fs.writeFileSync('certificates.zip', zipBuffer);
  console.log(`${attendees.length} certificates saved to certificates.zip`);
}
 
generateCertificates('attendees.csv');

The script sends 10 parallel requests per batch. Each batch completes in 200-400ms, so 200 certificates finish in about 20 batches (roughly 8-10 seconds of render time). PDF4.dev's dashboard also has a built-in batch feature: upload a CSV, map columns, and download a ZIP with no code.

How to secure generated certificates

Certificates are trust documents. Three measures prevent forgery and unauthorized distribution.

Watermarks for drafts. Add a diagonal "DRAFT" or "SAMPLE" overlay to preview copies. PDF4.dev's watermark tool applies text overlays to existing PDFs without re-rendering. See the watermark guide for options.

Password protection. Encrypt the final PDF so only the recipient can open it. Generate the certificate first, then pass the buffer through encryption. For details on RC4-128 (browser-side) vs AES-256 (qpdf), see the password protection guide.

Unique certificate IDs. Every certificate should include an ID like CERT-2026-00042 and optionally a verification URL (https://yoursite.com/verify/CERT-2026-00042). Store IDs in your database alongside recipient data so employers can verify authenticity.

How to adapt certificates for different use cases

The same template works for multiple certificate types by adjusting the data object. Use {{#if}} blocks for fields that only apply to certain types.

Use caseKey variablesNotes
Course completioncourse_name, hours, dateInclude duration and instructor
Event attendanceevent_name, event_date, locationAdd event logo via base64
Employee awardaward_title, achievementBolder heading typography
Professional accreditationcredential, license_number, expiryShow expiry date
Workshop participationworkshop_name, facilitator, skillsList specific skills

Embed logos as base64 data URIs (data:image/png;base64,...) to avoid network dependency during rendering. This adds a few KB to the template but guarantees the image appears in every PDF.

PDF4.dev includes a certificate starter template in the dashboard. Sign up free and generate your first certificate in under two minutes.

Convert any HTML to PDFTry it free

FAQ

How do I generate PDF certificates programmatically?

Create an HTML template with Handlebars variables for recipient name, course title, date, and certificate ID. Then call PDF4.dev's render API with the template ID and recipient data. Each call returns a finished PDF in 200-400ms.

Can I generate certificates in bulk from a CSV file?

Yes. Parse the CSV into an array, then call the render API once per recipient. With 10 parallel requests, 200 certificates take under 30 seconds. Bundle results into a ZIP, or use PDF4.dev's built-in batch feature in the dashboard.

What paper size should certificates use?

Landscape A4 (297 x 210mm) is the standard. PDF4.dev supports this via the a4-landscape preset in the Format panel or { "preset": "a4-landscape" } in the API.

How do I use custom fonts like Playfair Display?

Add the Google Fonts URL to the google_fonts_url field in your template's pdf_format. PDF4.dev loads the font during rendering. Example: https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&display=swap.

Can I add a watermark to generated certificates?

Yes. Use PDF4.dev's watermark tool to overlay text like "SAMPLE" or "COPY" diagonally across each page after generating the PDF.

How do I prevent certificate forgery?

Three layers: password protection encrypts the file, flattening removes editable fields, and a unique certificate ID with a verification URL lets anyone confirm authenticity against your database.

Free tools mentioned:

Html To PdfTry it freeMerge PdfTry it freeWatermark PdfTry it freeProtect PdfTry it freeCompress PdfTry it free

Start generating PDFs

Build PDF templates with a visual editor. Render them via API from any language in ~300ms.