Certificates Are Just Templates with Names
You run a course platform. A student completes a course, and you need to issue a certificate. The certificate has a fixed layout — logo, decorative borders, course title, completion date, a signature — and two pieces of dynamic data: the recipient’s name and the date.
At small scale, you do this manually. Open the Canva template, type the name, export the PDF, email it. Ten certificates a month? Tedious but manageable.
At scale, it breaks. A hundred students complete a cohort. A corporate training program graduates 500 employees. A conference issues certificates to every attendee. You’re not opening Canva 500 times.
The Image Generation API turns certificate generation into an API call. Define the template as layers — background, decorative elements, text fields — and swap the dynamic content per recipient. One HTTP request, one certificate.
The Certificate Template
A typical certificate has these elements:
- A background — solid color or a branded template image
- Decorative borders or accent lines
- The issuing organization’s logo
- A title like “Certificate of Completion”
- The recipient’s name (dynamic)
- The course or achievement title (dynamic)
- The completion date (dynamic)
- A signature or issuer name
Here’s a complete template:
type CertificateData = {
recipientName: string;
courseTitle: string;
completionDate: string;
signedBy: string;
};
const buildCertificateRequest = (certificate: CertificateData) => ({
dimensions: { width: 1600, height: 1131 },
output_format: "png" as const,
fonts: [
{
name: "Playfair Display",
weight: "Bold",
style: "normal",
file: {
type: "url",
name: "PlayfairDisplay-Bold.ttf",
url: "https://example.com/fonts/PlayfairDisplay-Bold.ttf",
},
},
{
name: "Inter",
weight: "Regular",
style: "normal",
file: {
type: "url",
name: "Inter-Regular.ttf",
url: "https://example.com/fonts/Inter-Regular.ttf",
},
},
{
name: "Inter",
weight: "Bold",
style: "normal",
file: {
type: "url",
name: "Inter-Bold.ttf",
url: "https://example.com/fonts/Inter-Bold.ttf",
},
},
],
layers: [
{
type: "solid-color-background",
index: 0,
hex_color: "#fefcf3",
opacity: 100,
},
{
type: "rectangle",
index: 1,
hex_color: "#1a1a2e",
position: { x: 40, y: 40 },
dimensions: { width: 1520, height: 1051 },
opacity: 8,
},
{
type: "rectangle",
index: 2,
hex_color: "#c9a84c",
position: { x: 60, y: 60 },
dimensions: { width: 1480, height: 2 },
opacity: 100,
},
{
type: "rectangle",
index: 3,
hex_color: "#c9a84c",
position: { x: 60, y: 1069 },
dimensions: { width: 1480, height: 2 },
opacity: 100,
},
{
type: "rectangle",
index: 4,
hex_color: "#c9a84c",
position: { x: 60, y: 60 },
dimensions: { width: 2, height: 1011 },
opacity: 100,
},
{
type: "rectangle",
index: 5,
hex_color: "#c9a84c",
position: { x: 1538, y: 60 },
dimensions: { width: 2, height: 1011 },
opacity: 100,
},
{
type: "static-image",
index: 6,
file: {
type: "url",
name: "logo.png",
url: "https://example.com/brand/logo.png",
},
position: { x: 680, y: 100 },
dimensions: { width: 240, height: 80 },
should_use_smart_cropping: false,
opacity: 100,
},
{
type: "text",
index: 7,
text: "Certificate of Completion",
font_name: "Playfair Display",
font_weight: "Bold",
font_size_in_px: 44,
text_color: "#1a1a2e",
text_align: "center",
position: { x: 200, y: 220 },
dimensions: { width: 1200, height: 60 },
opacity: 100,
},
{
type: "rectangle",
index: 8,
hex_color: "#c9a84c",
position: { x: 650, y: 300 },
dimensions: { width: 300, height: 2 },
opacity: 100,
},
{
type: "text",
index: 9,
text: "This is to certify that",
font_name: "Inter",
font_weight: "Regular",
font_size_in_px: 20,
text_color: "#666666",
text_align: "center",
position: { x: 200, y: 360 },
dimensions: { width: 1200, height: 30 },
opacity: 100,
},
{
type: "text",
index: 10,
text: certificate.recipientName,
font_name: "Playfair Display",
font_weight: "Bold",
font_size_in_px: 52,
text_color: "#1a1a2e",
text_align: "center",
position: { x: 200, y: 410 },
dimensions: { width: 1200, height: 70 },
opacity: 100,
},
{
type: "text",
index: 11,
text: "has successfully completed",
font_name: "Inter",
font_weight: "Regular",
font_size_in_px: 20,
text_color: "#666666",
text_align: "center",
position: { x: 200, y: 510 },
dimensions: { width: 1200, height: 30 },
opacity: 100,
},
{
type: "text",
index: 12,
text: `**${certificate.courseTitle}**`,
font_name: "Inter",
font_weight: "Regular",
font_size_in_px: 28,
text_color: "#1a1a2e",
text_align: "center",
position: { x: 200, y: 560 },
dimensions: { width: 1200, height: 80 },
is_splitting_lines: true,
opacity: 100,
},
{
type: "rectangle",
index: 13,
hex_color: "#c9a84c",
position: { x: 650, y: 680 },
dimensions: { width: 300, height: 2 },
opacity: 100,
},
{
type: "text",
index: 14,
text: certificate.completionDate,
font_name: "Inter",
font_weight: "Regular",
font_size_in_px: 18,
text_color: "#666666",
text_align: "center",
position: { x: 200, y: 720 },
dimensions: { width: 500, height: 30 },
opacity: 100,
},
{
type: "text",
index: 15,
text: certificate.signedBy,
font_name: "Inter",
font_weight: "Bold",
font_size_in_px: 18,
text_color: "#1a1a2e",
text_align: "center",
position: { x: 900, y: 720 },
dimensions: { width: 500, height: 30 },
opacity: 100,
},
{
type: "text",
index: 16,
text: "Date",
font_name: "Inter",
font_weight: "Regular",
font_size_in_px: 14,
text_color: "#999999",
text_align: "center",
position: { x: 200, y: 750 },
dimensions: { width: 500, height: 20 },
opacity: 100,
},
{
type: "text",
index: 17,
text: "Instructor",
font_name: "Inter",
font_weight: "Regular",
font_size_in_px: 14,
text_color: "#999999",
text_align: "center",
position: { x: 900, y: 750 },
dimensions: { width: 500, height: 20 },
opacity: 100,
},
],
});
That’s a lot of layers. But each one maps to a specific visual element — borders, text fields, dividers, logo. The structure is explicit. No CSS cascade, no layout engine, no surprises.
Generating a Single Certificate
import { IterationLayer } from "iterationlayer";
const client = new IterationLayer({ apiKey: "YOUR_API_KEY" });
const generateCertificate = async (certificate: CertificateData) => {
const requestBody = buildCertificateRequest(certificate);
const result = await client.generateImage(requestBody);
return Buffer.from(result.data.buffer, "base64");
};
const imageBuffer = await generateCertificate({
recipientName: "Alice Chen",
courseTitle: "Advanced TypeScript Patterns",
completionDate: "February 15, 2026",
signedBy: "Dr. Sarah Williams",
});
One call, one certificate. The template function handles the layout. The dynamic data — name, course, date, signer — slots into the text layers.
Batch Generation
The real use case is generating certificates for an entire cohort at once:
type Recipient = {
name: string;
email: string;
};
const generateCohortCertificates = async (
recipients: Recipient[],
courseTitle: string,
completionDate: string,
signedBy: string,
) => {
const certificates: Array<{ email: string; imageBuffer: Buffer }> = [];
for (const recipient of recipients) {
const imageBuffer = await generateCertificate({
recipientName: recipient.name,
courseTitle,
completionDate,
signedBy,
});
certificates.push({ email: recipient.email, imageBuffer });
}
return certificates;
};
Loop over your recipient list, generate each certificate, then email or store them. The API handles the rendering. Your code handles the orchestration.
For large batches — 500+ certificates — consider adding concurrency control. Run 5-10 requests in parallel to speed up generation without overwhelming your HTTP client.
Custom Fonts Matter
Certificates look cheap with system fonts. The difference between a certificate set in Arial and one set in Playfair Display is the difference between a screenshot and a document worth framing.
The API accepts TTF, OTF, WOFF, and WOFF2 font files, referenced by URL. You’re not limited to Google Fonts — upload any licensed font. Serif fonts for the title, sans-serif for the body text, a script font for the signature line.
Each font is specified with a name, weight, and style. Text layers reference the font by name and weight, so you can mix fonts within the same certificate:
{
"text": "Certificate of Completion",
"font_name": "Playfair Display",
"font_weight": "Bold",
"font_size_in_px": 44
}
{
"text": "has successfully completed",
"font_name": "Inter",
"font_weight": "Regular",
"font_size_in_px": 20
}
Two fonts, two visual roles. The serif font carries the formal weight. The sans-serif font carries the informational text.
Design Variations
The same layer structure supports different certificate designs by changing colors, fonts, and element positions:
- Corporate training — company brand colors, logo in the header, minimalist layout with lots of white space
- Academic courses — traditional cream background, gold (#c9a84c) accent lines, serif title font
- Conference attendance — dark background, bold modern fonts, event branding as an image overlay at low opacity
- Professional certifications — structured layout with credential ID, expiry date, and a QR code as a static-image layer
The template function pattern stays the same. You define a buildCertificateRequest variant for each design, and the generation logic doesn’t change.
Why Not PDF
Certificates are often distributed as PDFs. The Image Generation API produces images (PNG, JPEG, WebP, AVIF), not PDFs.
For most digital use cases, an image works better. Recipients share certificates on LinkedIn — that’s an image upload. They add them to portfolios — image embeds. They post them on social media — images.
If you need PDF output, generate the certificate image with the API, then embed it in a PDF using any PDF library. The image is the hard part — building the visual composition with fonts, colors, and positioning. Wrapping it in a PDF container is trivial.
Get Started
Check the docs for the full layer reference, including all layer types, font handling, and output format options.
Sign up at iterationlayer.com for a free API key — no credit card required. Build a certificate template with your brand fonts and colors, generate one for yourself, and see how it looks.