Dynamic Certificate Generation with HTML and a Render API

Certificates are one of those features that look trivial on paper and take days in practice. You need personalized text on a consistent design at high resolution, deliverable as a downloadable image, ideally without involving Canva, Photoshop, or a third-party service that charges per certificate. The fastest path is one you probably already know: HTML and CSS.

Design your certificate in HTML exactly as you would design a web page. Then use a render API to convert that HTML to a pixel-perfect PNG on demand. This tutorial walks through the full pipeline — from template design to a working API endpoint your users can hit to download their certificate.

Designing the certificate template

A certificate is a fixed-size image with a known layout. Use position: absolute for precise placement, set the body to the exact pixel dimensions you want, and use overflow: hidden to prevent anything from spilling outside the frame. Here is a minimal but polished template:

html — certificate template
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=Inter:wght@300;400&display=swap" rel="stylesheet">
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    width: 1400px;
    height: 990px;
    overflow: hidden;
    background: #fdfbf7;
    font-family: 'Inter', sans-serif;
    position: relative;
  }
  .border-frame {
    position: absolute;
    inset: 24px;
    border: 2px solid #c9a84c;
    border-radius: 4px;
  }
  .border-inner {
    position: absolute;
    inset: 32px;
    border: 1px solid rgba(201,168,76,.4);
    border-radius: 2px;
  }
  .content {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    padding: 80px;
    text-align: center;
    gap: 20px;
  }
  .issuer {
    font-size: 13px;
    font-weight: 400;
    letter-spacing: .18em;
    text-transform: uppercase;
    color: #8a7a5a;
  }
  .presents {
    font-family: 'Playfair Display', serif;
    font-style: italic;
    font-size: 22px;
    color: #a08040;
  }
  .recipient {
    font-family: 'Playfair Display', serif;
    font-size: 56px;
    font-weight: 700;
    color: #1a1a1a;
    line-height: 1.1;
  }
  .divider {
    width: 120px;
    height: 1px;
    background: #c9a84c;
  }
  .course {
    font-size: 18px;
    font-weight: 300;
    color: #444;
    max-width: 700px;
    line-height: 1.5;
  }
  .date {
    font-size: 13px;
    color: #8a7a5a;
    letter-spacing: .08em;
  }
</style>
</head>
<body>
  <div class="border-frame"></div>
  <div class="border-inner"></div>
  <div class="content">
    <div class="issuer">Acme Academy</div>
    <div class="presents">This certifies that</div>
    <div class="recipient">Jane Smith</div>
    <div class="divider"></div>
    <div class="course">has successfully completed <strong>Advanced TypeScript Patterns</strong></div>
    <div class="date">Awarded on February 20, 2026</div>
  </div>
</body>
</html>

Open this in a browser first to verify the layout before involving the API. Getting pixel-perfect certificates right in HTML is much faster than iterating through an API call every time.

For print-quality output, use deviceScaleFactor: 2 in your render request. This doubles the pixel density — a 1400×990 template renders as a 2800×1980 image — giving crisp results when printed at A4 or Letter size.

Building the API endpoint

Create a route that accepts name, course, and date as query parameters, builds the HTML, and returns the PNG. Here is a Next.js Route Handler, but the same pattern works in Express, Hono, or any Node.js HTTP framework:

ts — app/certificate/route.ts
import { NextRequest } from 'next/server';

const safe = (s: string) => s.replace(/[<>&"]/g, '').slice(0, 120);

export async function GET(req: NextRequest) {
  const p      = req.nextUrl.searchParams;
  const name   = safe(p.get('name')   ?? 'Recipient');
  const course = safe(p.get('course') ?? 'Course');
  const date   = safe(p.get('date')   ?? new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }));

  const html = buildCertificateHtml({ name, course, date });

  const res = await fetch('https://renderpix.dev/v1/render', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Api-Key': process.env.RENDERPIX_API_KEY!,
    },
    body: JSON.stringify({
      html,
      width: 1400,
      height: 990,
      format: 'png',
      deviceScaleFactor: 2,
    }),
  });

  if (!res.ok) {
    return new Response('Render failed', { status: 500 });
  }

  return new Response(await res.arrayBuffer(), {
    headers: {
      'Content-Type': 'image/png',
      'Content-Disposition': `attachment; filename="certificate-${name.replace(/\s+/g, '-').toLowerCase()}.png"`,
      'Cache-Control': 'public, max-age=31536000, immutable',
    },
  });
}

The Content-Disposition: attachment header triggers a browser download rather than opening the image inline. The long Cache-Control TTL is safe here because the certificate content is fully determined by the URL parameters — the same URL will always produce the same image.

Triggering the download from the UI

Wire the endpoint to a download button. A plain anchor tag with the download attribute is all you need:

tsx — DownloadButton.tsx
interface Props {
  name: string;
  course: string;
  completedAt: string; // ISO date string
}

export function CertificateDownload({ name, course, completedAt }: Props) {
  const date = new Date(completedAt).toLocaleDateString('en-US', {
    year: 'numeric', month: 'long', day: 'numeric',
  });

  const url = `/certificate?name=${encodeURIComponent(name)}&course=${encodeURIComponent(course)}&date=${encodeURIComponent(date)}`;

  return (
    <a href={url} download>
      Download Certificate
    </a>
  );
}

Handling long names and titles

The biggest practical challenge with certificate generation is typography overflow. A 14-character name like "Jane Smith" fits perfectly at 56 px. A 36-character name like "Alexandra Konstantinopoulou-Bergmann" does not. Two strategies work well:

The first approach is simpler and handles most real-world names correctly. The second is worth adding only if you have very long names or multilingual content where character count is a poor proxy for rendered width.

For a full certificate generation API overview, see certificate generator API.

Start generating certificates today.

A free account includes 100 renders per month — enough to build and test your entire certificate pipeline.

← All articles API Reference →