PDFPipe

Use case

Certificate PDF API

Generate course completion certificates, professional credentials, and award certificates as landscape PDFs. One HTTP call, your own HTML design, 500 free per month.

Landscape A4 or Letter

Most certificate designs are landscape. Set format: 'A4' and landscape: true. US Letter sizes are also supported.

Web fonts and decorative CSS

The renderer loads @import fonts, supports CSS variables, borders, outlines, and any layout that works in a browser: gradients, shadows, all of it.

Store and email in one call

Add store: true to get back a stable URL for emailing. No extra storage setup. Retention window depends on your plan (1 year on Growth+).

Unique certificate IDs

Generate a short UUID in your app, embed it in the template, and save it to your database. Verify authenticity later without calling the API.

Batch backfill

Starter+ plans can batch up to 10-500 certificates per call. A webhook fires when all are done, so you can update your database without polling.

Zero new dependencies

No headless browser binary to install, no Docker layer to maintain. Works from any language with an HTTP client.

1. Design the certificate template

Landscape A4, double border, Playfair Display for the recipient name. No images to upload, no proprietary template editor. Just HTML and CSS.

templates/certificate.html
<!-- templates/certificate.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <style>
    @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,700;1,400&family=Inter:wght@400;500&display=swap');
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { width: 297mm; height: 210mm; overflow: hidden; background: #fffdf8; }
    .cert {
      width: 100%; height: 100%;
      border: 12px solid #1d1812;
      outline: 3px solid #d23a1d;
      outline-offset: -20px;
      display: flex; flex-direction: column;
      align-items: center; justify-content: center;
      gap: 20px; padding: 60px;
      font-family: 'Inter', system-ui, sans-serif;
    }
    .label { font-size: 11px; letter-spacing: 0.3em; text-transform: uppercase; color: #888; }
    .org { font-family: 'Playfair Display', serif; font-size: 22px; color: #1d1812; }
    .certifies { font-size: 13px; color: #555; }
    .recipient {
      font-family: 'Playfair Display', serif; font-style: italic;
      font-size: 44px; color: #1d1812; text-align: center; line-height: 1.1;
    }
    .course-label { font-size: 12px; color: #888; }
    .course {
      font-family: 'Playfair Display', serif; font-size: 24px; font-weight: 700;
      color: #1d1812; text-align: center;
    }
    .meta { display: flex; gap: 60px; margin-top: 10px; }
    .meta-item { text-align: center; }
    .meta-value { font-size: 14px; font-weight: 500; color: #1d1812; }
    .meta-label { font-size: 10px; color: #888; margin-top: 3px; letter-spacing: 0.2em; text-transform: uppercase; }
    .divider { width: 120px; height: 1px; background: #d23a1d; }
    .cert-id { font-size: 10px; color: #bbb; margin-top: 4px; }
  </style>
</head>
<body>
  <div class="cert">
    <div class="label">Certificate of completion</div>
    <div class="org">{{organizationName}}</div>
    <div class="divider"></div>
    <div class="certifies">This certifies that</div>
    <div class="recipient">{{recipientName}}</div>
    <div class="course-label">has successfully completed</div>
    <div class="course">{{courseName}}</div>
    <div class="meta">
      <div class="meta-item">
        <div class="meta-value">{{completionDate}}</div>
        <div class="meta-label">Date</div>
      </div>
      <div class="meta-item">
        <div class="meta-value">{{duration}}</div>
        <div class="meta-label">Duration</div>
      </div>
      {{#if grade}}
      <div class="meta-item">
        <div class="meta-value">{{grade}}</div>
        <div class="meta-label">Grade</div>
      </div>
      {{/if}}
    </div>
    <div class="cert-id">Certificate ID: {{certId}}</div>
  </div>
</body>
</html>

2. Issue one certificate

Generate a short UUID in your app and embed it in the template as a certificate ID. The stable URL comes back in the response.

certificate.ts
// Generate a certificate and return a stable URL

import Handlebars from "handlebars";
import { readFileSync } from "fs";
import { randomUUID } from "crypto";

const tmpl = Handlebars.compile(readFileSync("./templates/certificate.html", "utf-8"));

interface CertData {
  organizationName: string;
  recipientName: string;
  courseName: string;
  completionDate: string;
  duration: string;
  grade?: string;
}

export async function issueCertificate(data: CertData): Promise<{ url: string; id: string }> {
  const certId = randomUUID().slice(0, 8).toUpperCase();
  const html = tmpl({ ...data, certId });

  const resp = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.PDFPIPE_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      html,
      store: true,
      filename: `cert-${certId}.pdf`,
      options: {
        format: "A4",
        landscape: true,
        margin: { top: "0", bottom: "0", left: "0", right: "0" },
        print_background: true,
      },
    }),
  });

  if (!resp.ok) throw new Error(`Certificate generation failed: ${resp.status}`);
  const { document_url } = await resp.json();
  return { url: document_url, id: certId };
}

3. Issue on course completion

Wire this to your LMS or course webhook. The certificate URL goes into the database and the email in the same request.

webhook.ts
// Issue certificates on course completion event (e.g. LMS webhook)

app.post("/webhooks/course-completed", async (req, res) => {
  const { userId, courseId } = req.body;
  const [user, course] = await Promise.all([
    db.users.findById(userId),
    db.courses.findById(courseId),
  ]);

  const { url, id } = await issueCertificate({
    organizationName: "Acme Academy",
    recipientName: `${user.firstName} ${user.lastName}`,
    courseName: course.title,
    completionDate: new Date().toLocaleDateString("en-US", {
      month: "long", day: "numeric", year: "numeric",
    }),
    duration: `${course.hours} hours`,
    grade: course.passingScore ? "Pass" : undefined,
  });

  await db.certificates.create({ userId, courseId, url, id });
  await sendEmail({
    to: user.email,
    subject: `Your certificate for ${course.title}`,
    body: `Congratulations! Download your certificate: ${url}`,
  });

  res.json({ certId: id, certUrl: url });
});

Batch-issue for past completions

Growth+ plans can batch up to 50 certificates per call with a webhook on completion.

backfill.ts
// Backfill certificates for all past completions

import { readFileSync } from "fs";
import Handlebars from "handlebars";

const tmpl = Handlebars.compile(readFileSync("./templates/certificate.html", "utf-8"));

const completions = await db.courseCompletions.findMany({
  where: { certificate: null },
  include: { user: true, course: true },
});

const batch = completions.slice(0, 50).map(({ user, course, completedAt }) => ({
  html: tmpl({
    organizationName: "Acme Academy",
    recipientName: `${user.firstName} ${user.lastName}`,
    courseName: course.title,
    completionDate: completedAt.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }),
    duration: `${course.hours} hours`,
    certId: user.id.slice(0, 8).toUpperCase(),
  }),
  filename: `cert-${user.id}.pdf`,
}));

const resp = await fetch("https://api.pdfpipe.xyz/v1/pdf/batch", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.PDFPIPE_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    options: { format: "A4", landscape: true, print_background: true },
    requests: batch,
    webhook_url: "https://yourapp.com/webhooks/certs-done",
  }),
});

Pricing

PlanPriceCertificates / monthStored retention
HobbyFree5001 day
Starter$193,00030 days
Growth$4915,0001 year
Scale$14950,0001 year
Business$499100,0001 year

Start issuing certificates

500 certificates a month free. No credit card. Key issued instantly after signup.

Related use case

Report PDF API →

Related use case

Invoice PDF API →

Related guide

PDF in Node.js →