Dynamic Certificate Generation for Online Courses (LMS)
If you run an online course platform, you know the drill: a student completes a course, and they expect a professional certificate they can share on LinkedIn or print for their wall.
Building PDF certificates from scratch using PHP libraries like TCPDF or FPDF means wrestling with coordinates, fonts, and image placements. It is tedious and error-prone.
A better approach: design your certificate using HTML/CSS (which you already know), and let an API convert it to PDF. This tutorial shows you how to build an automated certificate generator using PHP and the free Apdf API.
Quick Start: The PHP Code
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Client;
$apiToken = 'YOUR_API_TOKEN_HERE';
$apiUrl = 'https://apdf.io/api/pdf/file/create';
$statusUrl = 'https://apdf.io/api/job/status/check';
// Helper: poll until the async job finishes, then return its result.
$waitForJob = function (Client $client, string $jobId) use ($apiToken, $statusUrl) {
for ($i = 0; $i < 1200; $i++) {
$res = $client->post($statusUrl, [
'headers' => ['Authorization' => 'Bearer ' . $apiToken],
'form_params' => ['id' => $jobId],
]);
$body = json_decode($res->getBody(), true);
if ($body['status'] === 'successful') {
return $body['result'];
}
if ($body['status'] === 'failed') {
throw new RuntimeException($body['error'] ?? 'Job failed');
}
sleep(2);
}
throw new RuntimeException('Job did not finish in time');
};
// Student data (from your database)
$student = [
'name' => 'Sarah Johnson',
'course' => 'Advanced PHP Development',
'date' => 'December 5, 2025',
'instructor' => 'John Smith',
'certificate_id' => 'CERT-2025-001847'
];
// Certificate HTML template
$html = '
<!DOCTYPE html>
<html>
<head>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Georgia, serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #fff;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.certificate {
width: 900px;
padding: 60px;
text-align: center;
border: 3px solid #d4af37;
margin: 20px;
background: rgba(255,255,255,0.03);
}
.header { font-size: 14px; letter-spacing: 3px; color: #d4af37; }
.title { font-size: 48px; margin: 20px 0; color: #d4af37; }
.subtitle { font-size: 18px; margin-bottom: 30px; color: #ccc; }
.student-name { font-size: 36px; font-style: italic; margin: 20px 0; }
.course-name { font-size: 24px; color: #d4af37; margin: 20px 0; }
.details { font-size: 14px; color: #999; margin-top: 40px; }
.footer { display: flex; justify-content: space-between; margin-top: 50px; }
.footer div { text-align: center; }
.line { width: 150px; border-top: 1px solid #666; margin: 0 auto 10px; }
</style>
</head>
<body>
<div class="certificate">
<div class="header">CERTIFICATE OF COMPLETION</div>
<div class="title">Certificate</div>
<div class="subtitle">This is to certify that</div>
<div class="student-name">' . $student['name'] . '</div>
<div class="subtitle">has successfully completed the course</div>
<div class="course-name">' . $student['course'] . '</div>
<div class="details">
Completed on ' . $student['date'] . '<br>
Certificate ID: ' . $student['certificate_id'] . '
</div>
<div class="footer">
<div>
<div class="line"></div>
<div>' . $student['instructor'] . '</div>
<div style="color:#666;font-size:12px;">Instructor</div>
</div>
<div>
<div class="line"></div>
<div>Academy Director</div>
<div style="color:#666;font-size:12px;">Signature</div>
</div>
</div>
</div>
</body>
</html>';
$client = new Client();
try {
$response = $client->post($apiUrl, [
'headers' => [
'Authorization' => 'Bearer ' . $apiToken,
'Accept' => 'application/json',
'Content-Type' => 'application/json',
],
'json' => [
'html' => $html,
'orientation' => 'landscape',
'format' => 'a4',
'margin_top' => '0',
'margin_bottom' => '0',
'margin_left' => '0',
'margin_right' => '0'
]
]);
$jobId = json_decode($response->getBody(), true)['job_id'];
$result = $waitForJob($client, $jobId);
echo "Certificate generated!\n";
echo "Download: " . $result['file'] . "\n";
} catch (\Exception $e) {
echo "Error: " . $e->getMessage();
}
Step-by-Step Breakdown
1. Set Up Your Project
composer require guzzlehttp/guzzle
Then get your API token from Apdf (it's free).
2. The Certificate Template
The HTML template above creates a professional dark-themed certificate. Key design choices:
- Landscape orientation: Certificates look better wide than tall
- Zero margins: The design extends to the edges
- Gold accents: The classic certificate color (#d4af37)
- Dynamic data: Student name, course, date, and ID are injected from your database
3. The API Parameters
The magic happens in the API request. These parameters are key:
'orientation' => 'landscape', // Wide format for certificates
'format' => 'a4', // Standard paper size
'margin_top' => '0', // Full-bleed design
The API accepts any valid HTML/CSS, so you can customize colors, add your logo via URL, or use custom fonts via Google Fonts.
Run the Script
php generate_certificate.php
Output
Certificate generated!
Download: https://apdf-files.s3.eu-central-1.amazonaws.com/abc123def456.pdf
That URL can be emailed directly to the student or saved to your database. Note: Download URLs are valid for 1 hour. If you need permanent storage, download the file and save it to your own storage.
Integrating with Your LMS
In a real LMS, you would trigger certificate generation when a student completes a course. Here is a simplified Laravel example:
// In your CourseController
public function complete(Course $course, User $user)
{
// Mark course as completed
$user->courses()->updateExistingPivot($course->id, [
'completed_at' => now()
]);
// Generate certificate
$certificateUrl = $this->generateCertificate($user, $course);
// Email to student
Mail::to($user)->send(new CourseCertificate($certificateUrl));
return redirect()->back()->with('success', 'Certificate sent!');
}
Next Steps
- Add a QR Code: Embed a verification URL in the certificate HTML so employers can verify authenticity
- Password Protect: Use the Password Protection endpoint to secure certificates with the student's ID
- Compress for Email: Use the Compress endpoint to reduce file size before emailing
Related tutorials
How to Generate PDFs from Claude
Describe the document, get the PDF: Claude drafts the HTML layout and renders it through the Apdf MCP tools — invoices, reports and one-pagers straight from the conversation, with page format, margins and headers steered in plain English. Revisions are just more chat.
Generate PDF Event Tickets with QR Codes in A6 Format using Go
Learn how to generate A6-sized event tickets with embedded QR codes using Go. This tutorial focuses on small-format PDFs (tickets, badges, labels) using CSS print media and the Apdf API. Perfect for event management systems, concerts, and conferences where scanning and compact printing are essential.
Build an Automated Invoice Generator using HTML & Python
The tutorial explains how to build a Python invoice generator by using Jinja2 to merge data into standard HTML/CSS templates. Instead of manually coding complex PDF layouts, the script sends the rendered HTML to the Apdf API, which instantly converts it into a professional, downloadable PDF.
Your code made the PDF.
Then it went dark.
Opened, read, re-read, dropped on page 4 — you never see any of it. Share the PDFs you generate through Apdf recipient links, and every signal becomes something you can act on: ping Slack, update the CRM, let an agent follow up. Same account, same API token, one more call.
API · Webhook · MCP