Skip to content
CastPDF

A certificate for every name on the list

A certificate generator API makes one personalised PDF for every learner, attendee or award winner. Your code sends a name, a course and a date. CastPDF fills a landscape certificate template and returns the finished file in about a second. Repeat it for each row of your list and everyone gets their own certificate.

  • Updated October 2026
  • 100 free PDFs a month

One request per person

A course platform, a training team and an event organiser all face the same small chore. Someone finishes, and they deserve a certificate with their own name on it. Doing that by hand in a design tool works for ten people. It stops working at a hundred, and it is easy to misspell a name along the way.

With an API, the design is fixed and the details are data. Each request carries one person’s name, course, date and certificate number. Each response is one finished PDF, ready to email or offer as a download. Nobody retypes a name, and every certificate looks the same.

Certificates are one use of our general PDF generation API. The same endpoint makes invoices and reports, so if you already use it for something else, a certificate is just another template ID.

The certificate starter

The dashboard includes a certificate of completion you can use as it is or restyle. It is A4 landscape with no page margin, a navy border, gold corner marks and a round seal. Its sample data describes one learner who finished a data visualisation course:

The starter’s sample data
{
  "certificate_id": "HA-2026-04817",
  "completed_on": "2026-09-18",
  "issuer": { "name": "Harbourline Academy", "short": "Harbourline", "verify_url": "harbourline.example/verify" },
  "recipient": { "name": "Amara Okafor" },
  "course": { "title": "Advanced Data Visualisation", "hours": 24, "format": "Instructor-led, online", "grade": "Awarded with distinction" },
  "signatory": { "name": "Elena Varga", "title": "Director of Studies" }
}
Where each field appears
FieldOn the certificate
recipient.nameThe large italic name in the middle
course.titleThe course line under the name
course.hours, course.format, course.gradeOne small line of details under the course
completed_onPrinted as a long date on the left, and its year on the seal
signatory.name, signatory.titleThe signature line on the right
issuer.name, issuer.shortThe heading above the title, and the text on the seal
certificate_id, issuer.verify_urlThe small line at the bottom, for checking it later

To make a certificate for someone else, send their details with your template ID. Any field you leave out simply prints nothing, so send all of them:

curl: one certificate for one learner
curl https://api.castpdf.com/v1/pdf \
  -H "Authorization: Bearer $CASTPDF_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cert-HA-2026-05102" \
  -d '{
    "template_id": "'"$CERTIFICATE_TEMPLATE_ID"'",
    "data": {
      "certificate_id": "HA-2026-05102",
      "completed_on": "2026-10-02",
      "issuer": {"name": "Harbourline Academy", "short": "Harbourline", "verify_url": "harbourline.example/verify"},
      "recipient": {"name": "Tomás Lindqvist"},
      "course": {"title": "Practical SQL for Analysts", "hours": 16, "format": "Self-paced, online", "grade": "Completed"},
      "signatory": {"name": "Elena Varga", "title": "Director of Studies"}
    },
    "filename": "certificate-HA-2026-05102"
  }' \
  --output certificate-HA-2026-05102.pdf

The starter sets its page in the template settings, not in its CSS, and its layout stretches to fill whatever page it gets. That is why the printing options further down work without editing the design. More styles are on the certificate template page.

Names, dates, signatures, logos and unique IDs

Names

Names are the one thing people check first. Send each name exactly as the person wrote it, accents and all. Output is HTML-escaped automatically, so a name with an ampersand or an angle bracket prints as text and cannot break the layout.

Very long names are the usual surprise. Type the longest name on your list into the preview before you go live. If it wraps badly, give long names a smaller size with a short Liquid check:

Liquid: a smaller font for long names
{% assign name_length = recipient.name | size %}
<p class="recipient{% if name_length > 26 %} recipient-long{% endif %}">{{ recipient.name }}</p>

Then add a rule such as .recipient-long { font-size: 30pt; } to the CSS. Noto fonts for many alphabets are installed on the renderer, so names in Greek, Cyrillic, Arabic or Chinese have fonts available. Add the matching Noto family to your font stack if the design needs a particular look.

Dates

The starter prints completed_on with format_date: "long", "UTC", "en-GB", which reads as 18 September 2026. Change the locale to en-US for September 18, 2026, or to fr-FR for French. Send dates as plain YYYY-MM-DD strings and they never shift by a day.

Signatures and logos

Out of the box, the signer’s name is set in a script-like italic. For a real signature, scan it, save it as a PNG with a transparent background and add it to the template as an image. A logo works the same way.

  • Host images at a public https address, or embed them in the template as data: URLs so nothing needs to load.
  • Addresses on localhost, on private networks or behind a login cannot be fetched by the renderer.
  • A signature image that differs per signer can come from your data, as an image address in a field.

Unique IDs and checking

Each certificate should carry a number from your own system, such as HA-2026-05102, plus the address where someone can check it. CastPDF prints both. The checking page itself lives on your website: it looks up the number in your records and confirms the name and course.

There is no QR code helper in CastPDF today. If you want a scannable code, create the QR image in your own code with a library you trust. Then send it as a data: URL in your data and place it with an <img> tag.

Making hundreds at once

Here is the honest picture. There is no bulk button in the dashboard yet. Through the API you can send a whole class in one batch request: one item per person, or a CSV or XLSX file. You then download a ZIP when it is done, as the batch API describes. Or your code sends one request per person, one after another, which is the approach below.

In practice that is a short script. Export your list from your spreadsheet app or your course platform as a CSV file:

learners.csv
email,name,course,hours,grade,certificate_id,completed_on
[email protected],Amara Okafor,Practical SQL for Analysts,16,Completed,HA-2026-05101,2026-10-02
[email protected],Tomás Lindqvist,Practical SQL for Analysts,16,Completed with merit,HA-2026-05102,2026-10-02
[email protected],"Mei Chen, PhD",Practical SQL for Analysts,16,Completed,HA-2026-05103,2026-10-02

Then loop over the rows. This Python script uses the built-in csv module, which copes with commas inside quoted names. It sends each learner’s certificate number as the Idempotency-Key and writes every download link to a second file:

Python: one certificate per CSV row
import csv
import os
import time

import requests

API = "https://api.castpdf.com/v1/pdf"
API_KEY = os.environ["CASTPDF_API_KEY"]
TEMPLATE_ID = os.environ["CERTIFICATE_TEMPLATE_ID"]


def make_certificate(row):
    key = f"cert-{row['certificate_id']}"
    body = {
        "template_id": TEMPLATE_ID,
        "data": {
            "certificate_id": row["certificate_id"],
            "completed_on": row["completed_on"],
            "issuer": {"name": "Harbourline Academy", "short": "Harbourline", "verify_url": "harbourline.example/verify"},
            "recipient": {"name": row["name"]},
            "course": {"title": row["course"], "hours": int(row["hours"]), "format": "Self-paced, online", "grade": row["grade"]},
            "signatory": {"name": "Elena Varga", "title": "Director of Studies"},
        },
        "filename": key,
        "response": "url",
    }
    headers = {"Authorization": f"Bearer {API_KEY}", "Idempotency-Key": key}
    for attempt in range(5):
        res = requests.post(API, headers=headers, json=body, timeout=60)
        if res.status_code in (409, 429, 503):
            time.sleep(int(res.headers.get("Retry-After", 2 ** attempt)))
            continue
        res.raise_for_status()
        return res.json()["url"]
    raise RuntimeError(f"gave up on {key}")


with open("learners.csv", newline="", encoding="utf-8") as src, open("links.csv", "w", newline="", encoding="utf-8") as dst:
    writer = csv.writer(dst)
    writer.writerow(["email", "name", "certificate_id", "url"])
    for row in csv.DictReader(src):
        url = make_certificate(row)
        writer.writerow([row["email"], row["name"], row["certificate_id"], url])
        print("done:", row["name"])

The loop waits for each certificate before it asks for the next, so it never trips the limit on renders running at once. If the script stops halfway, run it again within 24 hours. Rows that already succeeded return their first PDF through the Idempotency-Key and are not counted twice. The Python developer page adds logging and error handling, and the Node.js page shows the same ideas in JavaScript.

Your plan sets the pace. These limits apply per workspace:

Pace and allowance per plan
PlanRequests a minutePDFs rendering at oncePDFs a month
Free301100
Starter6022,500
Growth120310,000
Pro300450,000
Scale6004200,000

On Free, the rate limit alone lets a class of 100 through in about 4 minutes. On Growth, a cohort of 500 needs about 5 minutes. A one-at-a-time loop also waits for each render, so a real run can take longer. Test keys are limited to 20 a minute, so run your full list with a live key once the design is final. A request over the limit gets a 429 with a Retry-After header, and the script above simply waits.

Certificates the moment someone finishes

A batch script suits a cohort that ends on one day. Self-paced courses are different: learners finish at all hours, and they want the certificate while the feeling is fresh. For that, make the certificate in the same place your app records the completion.

Your course or event software usually has a moment you can hook into, such as a lesson marked complete, a final quiz passed or a badge scanned at the door. Your server reacts to that moment, creates the certificate number, calls CastPDF and saves the result next to the learner’s record.

Node.js 20+: issue a certificate when a course is completed
export async function issueCertificate(learner, course, certificateId) {
  const res = await fetch('https://api.castpdf.com/v1/pdf', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.CASTPDF_API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `cert-${certificateId}`,
    },
    body: JSON.stringify({
      template_id: process.env.CERTIFICATE_TEMPLATE_ID,
      data: {
        certificate_id: certificateId,
        completed_on: new Date().toISOString().slice(0, 10),
        issuer: { name: 'Harbourline Academy', short: 'Harbourline', verify_url: 'harbourline.example/verify' },
        recipient: { name: learner.fullName },
        course: { title: course.title, hours: course.hours, format: course.format, grade: learner.grade ?? 'Completed' },
        signatory: { name: 'Elena Varga', title: 'Director of Studies' },
      },
      filename: `certificate-${certificateId}`,
    }),
    signal: AbortSignal.timeout(60_000),
  });
  if (!res.ok) {
    const { error } = await res.json();
    throw new Error(`${error.code}: ${error.message}`);
  }
  return Buffer.from(await res.arrayBuffer());
}

Save the certificate number before you call the API. If the call fails, a background retry can use the same number and the same key, and the learner still ends up with exactly one certificate. Ready-made apps for course and event platforms are not available today, so this call always comes from your own server. The generate a PDF from JSON guide explains the template route in more depth.

Getting certificates to people

CastPDF makes the PDF. Sending it is up to your own email service or your platform, which already knows each learner’s address. There are two common ways to do it.

  • Send the link. With "response": "url", each certificate comes back as a signed link that works without an API key. Put it in your congratulations email.
  • Attach the file. Without response, the answer is the PDF itself. Attach it to the email, or save it to your own storage first.

Signed links expire when your plan’s storage time ends, from 1 day on Free to 90 days on Scale. A certificate is something people keep for years. Copy each PDF into your own storage and link to it from the learner’s account page, so the download keeps working.

Only need a handful? A non-technical colleague can do it in the dashboard: open the template, type the person’s details into the Simple mode form and press Generate PDF.

Printing: A4 landscape or US Letter

Most certificates are printed in landscape, on A4 in most of the world and on US Letter in the United States and Canada. The two sizes are close but not the same, so pick the one your recipients use.

Common certificate paper sizes
PaperSize in landscapeRequest fields
A4297 × 210 mm"format": "A4", "orientation": "landscape"
US Letter11 × 8.5 in (279 × 216 mm)"format": "Letter", "orientation": "landscape"
A5, for small awards210 × 148 mm"format": "A5", "orientation": "landscape"

The starter defaults to A4 landscape with no margin. Add "format": "Letter" to a request and the same design fills a Letter page instead. You can also change the default once in the template settings.

Home and office printers cannot print right to the edge of the paper. The starter already keeps its border away from the edge, but if people print at home, a small margin such as "margins": "6mm" adds a safe white frame. For a print shop, ask whether they want bleed before you send files.

What certificates cost

Each certificate is one PDF. The Free plan includes 100 live PDFs a month, which covers a small course, and its live PDFs carry a small "Made with CastPDF" line at the bottom. Starter is $19 a month for 2,500 PDFs with no footer, and Growth is $49 for 10,000.

Graduation season is uneven, and paid plans allow for it. Unused PDFs roll over for a month, and a busy month can go past the allowance up to a spending cap you choose. We email you at 80% and 100% of your PDFs. Every price is on the pricing page.

Go deeper

Everything about the certificate generator

Every guide, template and example that builds on this page. Each one answers a narrower question in more depth.

FAQ

Common questions

Is there a bulk upload for certificates in the dashboard?

Not today. The dashboard makes one certificate at a time with the Generate PDF button. For a whole class, a short script sends one request per person from a CSV file you export.

How many certificates can I make per minute?

It depends on your plan: 30 requests a minute on Free, 60 on Starter and up to 600 on Scale. Each request makes one certificate. Over the limit, the API answers 429 and says how long to wait.

What if my certificate script stops halfway through the list?

Run it again with the same Idempotency-Key for each person. Certificates that already succeeded come back as they were and are not counted again. Only the missing ones are rendered.

Can people check that a certificate is genuine?

Yes, through your own website. Print a unique certificate number and your checking address on each one. Your site then looks the number up in your records and shows who earned it.

Can I put a QR code on a certificate?

Yes, as an image you create. CastPDF has no QR code helper today, so generate the code in your own code and send it as an image address or data URL. The template places it like any other image.

Will a very long name fit on the certificate?

Try your longest name in the template preview first. If it wraps, a short Liquid check can switch long names to a smaller font size. Changing the design is ordinary HTML and CSS.

Do certificates carry a watermark?

Test-key certificates carry a large CASTPDF TEST watermark and are free. Live certificates on the Free plan have a small Made with CastPDF line at the bottom. Paid plans add nothing.

How do I give learners a download link that keeps working?

Store each PDF in your own storage and link to that copy. The signed links CastPDF returns expire when your plan’s storage time ends. They are meant for delivery, not for keeping.

Make your first PDF in 5 minutes

Pick a template, add your details and download your PDF. You get 100 free PDFs every month, and you don’t need a card.