Build a Certificate Generator with QR Layers

Published October 12, 2026 by Balaji

Uncategorized

Code for an API certificate generator: QR layer, PDF vs PNG, 50-job batches. Positioning page: /use-cases/certificates.

Overview

Certificates are one of those things every product needs at some point — course completions, event participation, webinar attendance, professional credentials — and one of the most painful things to produce manually. A 500-student cohort means 500 personalized certificates: names, course titles, dates, unique IDs, all rendered on a branded template without typos and without a three-day design queue.

This post walks through building a certificate generator with jsontoimg — a JSON-in, image-out API. You design the certificate template once, connect your participant data, and every completion triggers a personalized, verifiable certificate automatically. By the end you’ll have a production pipeline that turns a database row into a finished certificate in seconds.

This is the implementation deep dive for our certificate generator guide — here we get into the actual code.

Why Manual Certificate Generation Breaks

Most organizations start with a designer, a template file, and a mail merge. It works for the first cohort of 20. It collapses at 500:

  • Time: At 10–15 minutes per certificate (find record, edit name, export, proof), 500 graduates is 80–125 hours of work per cohort.
  • Errors: Name misspellings, wrong course titles, and inconsistent dates slip through manual edits. A certificate with a typo is worse than no certificate — it gets screenshotted and shared.
  • Latency: Participants expect their certificate immediately after completing a course, not after a “certificates will be sent within 3 business days” email.
  • Verification: Unique certificate IDs and QR codes must be generated per recipient — impossible to do reliably by hand.

The fix is the same pattern used for product banners and OG images: a template rendered from data. Certificates are mail-merge for images.

The Data Model: One Row Per Certificate

Every certificate needs the same shape of data. Define it once as JSON, and it drives both the render and the verification record:

{
  "certificate_id": "CERT-2026-004217",
  "recipient_name": "Priya Sharma",
  "course_title": "Advanced SQL for Data Analysts",
  "instructor": "Balaji R",
  "completion_date": "2026-08-01",
  "duration_hours": 12,
  "score_percent": 94,
  "course_id": "sql-201",
  "cohort": "Cohort 12"
}

Design your template with dynamic text layers for recipient_name, course_title, completion_date, and certificate_id. Everything else — logo, borders, signatures, background — stays static in the template. The API only swaps what’s dynamic.

Rendering a Single Certificate

With your template designed, rendering one certificate is a single POST request:

curl -X POST "https://app.jsontoimg.com/api/v1/render?wait=60" \
  -H "Authorization: Bearer $JSONTOIMG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "certificate-award-v2",
    "format": "png",
    "layers": {
      "certificate_id": { "text": "CERT-2026-004217" },
      "recipient_name": { "text": "Priya Sharma" },
      "course_title": { "text": "Advanced SQL for Data Analysts" },
      "completion_date": { "text": "2026-08-01" }
    }
  }'

The response is your finished certificate at full print resolution — no browser, no HTML-to-image hacks, no font-loading surprises. If you need a lighter web preview, request output: "jpg" at a smaller width for the same template.

Batch Generation: 500 Certificates, One Request

For a cohort that just graduated, fire them all at once. jsontoimg accepts an array of data objects and returns an array of images:

curl -X POST https://app.jsontoimg.com/api/v1/renders/batch \
  -H "Authorization: Bearer $JSONTOIMG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jobs": [
      {
        "template": "certificate-award-v2",
        "format": "png",
        "layers": {
          "certificate_id": { "text": "CERT-2026-004217" },
          "recipient_name": { "text": "Priya Sharma" },
          "course_title": { "text": "Advanced SQL for Data Analysts" },
          "completion_date": { "text": "2026-08-01" }
        }
      },
      {
        "template": "certificate-award-v2",
        "format": "png",
        "layers": {
          "certificate_id": { "text": "CERT-2026-004218" },
          "recipient_name": { "text": "Arjun Mehta" },
          "course_title": { "text": "Advanced SQL for Data Analysts" },
          "completion_date": { "text": "2026-08-01" }
        }
      }
    ]
  }'

Your LMS or course platform already has this data — you’re just exporting a few columns. Here’s the whole pipeline in Python against a typical database:

import requests
import sqlite3

API_KEY = "YOUR_API_KEY"
TEMPLATE_ID = "certificate-award-v2"

conn = sqlite3.connect("lms.db")
rows = conn.execute("""
    SELECT certificate_id, recipient_name, course_title, completion_date
    FROM completions
    WHERE certificate_generated = 0
""").fetchall()

jobs = [{
    "template": TEMPLATE_ID,
    "format": "png",
    "layers": {
        "certificate_id": { "text": r[0] },
        "recipient_name": { "text": r[1] },
        "course_title": { "text": r[2] },
        "completion_date": { "text": r[3] },
    },
} for r in rows[:50]]

resp = requests.post(
    "https://app.jsontoimg.com/api/v1/renders/batch",
    json={"jobs": jobs},
    headers={"Authorization": f"Bearer {API_KEY}"},
)
batch = resp.json()
print(batch["id"], batch.get("acceptedCount"))

That’s the whole generator: select ungenerated completions → render → save. Run it after every cohort closes, or on a schedule.

Async Delivery with Webhooks

For very large batches — an event with 10,000 attendees, a certification body with quarterly exam cycles — synchronous rendering ties up your request for too long. Use async mode with a webhook instead:

curl -X POST https://app.jsontoimg.com/api/v1/renders/batch \
  -H "Authorization: Bearer $JSONTOIMG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jobs": [ /* max 50; each job has template + layers; optional webhookUrl per job */ ]
  }'
# Then GET /api/v1/renders/batches/{id}

jsontoimg calls your webhook when each batch finishes, and your app emails the certificate or updates the participant record. The typical flow:

Webhook trigger (course completion)
  → Look up participant row
  → Render certificate (sync for singles, batch for cohorts)
  → Upload to storage / attach to email
  → Mark certificate_generated = 1

Verification: Unique IDs and QR Codes

A certificate without verification is just a nice picture. Put the certificate_id on the certificate itself, and optionally a QR code that resolves to a public verification page:

  1. Generate a unique ID per recipient in your database (CERT-2026-004217).
  2. Pass it into the render as a dynamic layer — it appears on the certificate.
  3. Publish a public endpoint: https://verify.yoursite.com/CERT-2026-004217 shows “Valid — issued to Priya Sharma for Advanced SQL for Data Analysts.”
  4. Render the same ID as a QR code layer so employers can scan and verify instantly.

This is the same pattern event platforms use for attendee badges and event tickets: unique identifier in, tamper-evident image out.

Two of the choices above have published guidance behind them. For the identifier, RFC 9562 is the current UUID specification and the reason to mint a v4 or v7 UUID rather than an incrementing integer: a sequential certificate number tells a holder how many you have issued and lets anyone guess a neighbour’s URL. For legibility, WCAG 2.1’s contrast minimum applies to a certificate as much as to a web page, and a recipient name set in light grey on a cream background fails it on every copy. The format call is the last one — MDN’s image file type guide covers PNG for the screen-shared version, while PDF is what survives being printed.

Real-World Numbers

An online learning platform with 5 concurrent courses, 100 graduates each per month:

Step Manual jsontoimg pipeline
Design template 1 designer, ~4 hours 1 designer, ~4 hours (once)
Generate 500 certificates 80–125 hours ~2 minutes of API time
Error rate Typos happen per-certificate Zero — data in, image out
Delivery time 3 days processing Minutes after completion
Verification None or manual lookup Built-in IDs + QR

The template cost is paid once; the marginal cost of the 501st certificate is a single API call.

Key Takeaways

  • Certificates are a data problem, not a design problem — one template plus JSON per recipient is the whole system.
  • Batch rendering handles entire cohorts in one request; webhooks make it async for events at any scale.
  • Unique IDs and QR codes turn generated certificates into verifiable credentials.
  • The same pipeline pattern applies across use cases: product banners, OG images, tickets, welcome graphics.

FAQ

How do I generate hundreds of certificates at once?

One row per recipient, then POST /api/v1/renders/batch in chunks of 50. Five hundred certificates is ten sequential batch requests.

Can the certificates come out as PDF instead of PNG?

Yes. Send format: "pdf" and a multi-page template becomes a multi-page PDF, billed one credit per page — the same as a still.

How do I make a generated certificate verifiable?

Mint a unique ID per recipient, render it into the certificate as text, and point a QR image layer at a verification URL containing that ID. The image becomes a lookup key rather than a claim.

Do I need a webhook for a certificate run?

For a cohort of a few dozen, waiting on the batch response is fine. Past a few hundred, pass a webhook so the run does not hold a connection open while the queue drains.

Related Resources

Zoom out: How JSON-to-Image Conversion Works maps the whole template, named-layer, and render-job architecture.