The html parameter takes a complete HTML string. Pass your certificate template with the student's name, course title, and completion date already injected, then set format=pdf. The API renders it in a full Chromium instance and returns a print-ready PDF. No template editor to learn. No proprietary design system to migrate into. Whatever your HTML looks like, that's what comes back.
Certificate generation has a few specifics that change how you configure the call, though. Orientation, DPI, font loading, and format selection all matter more here than they do for invoices or reports. This page covers those specifics.
Your HTML template, rendered exactly as designed
The usual path for course certificates starts with a Canva design someone exports manually. Or a WordPress plugin that renders badly on half the browsers it's opened in. Or a wkhtmltopdf integration that worked in 2019 but breaks on any CSS written after that.
The API approach is simpler. Write the certificate in HTML and CSS, pass it as the html parameter, and Chromium renders it. The output matches the design exactly, including Google Fonts, CSS gradients, box shadows, and SVG seals that wkhtmltopdf can't handle.
Student name, course title, issue date, certificate ID: inject these server-side before the API call, using whatever templating system you already have (Handlebars, Jinja2, Blade, ERB). The API receives a fully-composed HTML string. It doesn't need to know about your data model.
curl "https://api.screenshotrun.com/v1/screenshots/capture" \
-H "Authorization: Bearer YOUR_API_KEY" \
-G \
--data-urlencode "html=...certificate HTML here..." \
--data-urlencode "format=pdf" \
--data-urlencode "pdf_landscape=true" \
--data-urlencode "pdf_page_format=A4" \
--data-urlencode "retina=true" \
--data-urlencode "delay=500" \
--data-urlencode "cache_ttl=0" \
-o certificate.pdf
A few things in that call are worth explaining. pdf_landscape=true because virtually every certificate design is horizontal. cache_ttl=0 because each certificate is unique — you don't want the API returning a cached render with someone else's name on it. And delay=500 to give Google Fonts time to load before capture. Heavy font stacks may need 1000ms; if the certificate renders with fallback fonts, that's your first thing to increase.
Why retina=true matters for certificates specifically
Standard screenshot resolution is fine for thumbnails and web previews. But certificates get printed, framed, and uploaded to LinkedIn where people zoom in. Logos that look sharp on screen can pixelate in print at 1x resolution.
retina=true doubles the pixel density. An A4 landscape certificate at 1123×794px becomes 2246×1588px in the output. Text stays crisp, the seal stays sharp, and the PDF holds up when printed at full size. For any certificate that might go to paper, this is not optional.
What does that look like in practice? A standard Teachable certificate exported as PNG looks fine in Gmail. But open it at full size, or print it on A4, and the logo is noticeably soft. The same template with retina=true holds up in both contexts.
For PNG output, the same logic applies. When a learner uploads a certificate image to their LinkedIn profile, LinkedIn displays it at small dimensions but allows zooming. A 2x image handles that without artifacts.
One template, three output formats
Most certificate workflows need two deliverables: a PDF for the learner to download and print, and an image for sharing on LinkedIn or embedding in a completion email. Both come from the same HTML template. Change the format parameter and that's it.
| Format | Best for | Notes |
|---|---|---|
format=pdf |
Download, print, archival | Use with pdf_landscape=true + pdf_page_format=A4 |
format=png |
LinkedIn upload, email inline, web display | Lossless; transparent background via omit_background=true |
format=jpeg |
Bulk email attachment, low-storage archival | Smaller file, no transparency, fine for email previews |
Two API calls per certificate — one for PDF, one for PNG — covers every delivery channel. If your platform sends a completion email with a certificate preview image and a "Download PDF" button, that's exactly two calls per course completion event, running in parallel.
Triggering generation automatically on course completion
Every major LMS fires a webhook when a learner completes a course. That's your trigger. Your server receives the event, populates the HTML template with the learner's data, calls the API, and delivers the certificate. No manual steps, no batch jobs, no checking who finished what.
Here's a Node.js handler for a Teachable course completion webhook:
const express = require('express');
const axios = require('axios');
const Handlebars = require('handlebars');
const fs = require('fs');
const app = express();
app.use(express.json());
const template = Handlebars.compile(fs.readFileSync('./certificate.html', 'utf8'));
app.post('/webhooks/teachable/completion', async (req, res) => {
const { name, email, course_name, completed_at } = req.body;
const html = template({
studentName: name,
courseName: course_name,
issueDate: new Date(completed_at).toLocaleDateString('en-US', {
year: 'numeric', month: 'long', day: 'numeric',
}),
certificateId: `CERT-${Date.now()}`,
});
const apiParams = {
html,
pdf_landscape: true,
pdf_page_format: 'A4',
retina: true,
delay: 500,
cache_ttl: 0,
};
const headers = { Authorization: `Bearer ${process.env.SCREENSHOTRUN_KEY}` };
// Generate PDF and PNG in parallel
const [pdfRes, pngRes] = await Promise.all([
axios.get('https://api.screenshotrun.com/v1/screenshots/capture',
{ headers, params: { ...apiParams, format: 'pdf' }, responseType: 'arraybuffer' }),
axios.get('https://api.screenshotrun.com/v1/screenshots/capture',
{ headers, params: { ...apiParams, format: 'png' }, responseType: 'arraybuffer' }),
]);
await storeCertificate(email, pdfRes.data, pngRes.data);
await sendCompletionEmail(email, name, course_name);
res.sendStatus(200);
});
app.listen(3000);
For LearnDash on WordPress, the hook is learndash_course_completed:
add_action('learndash_course_completed', function($data) {
$user = get_userdata($data['user']->ID);
$course = get_post($data['course']->ID);
$html = render_certificate_template([
'student_name' => $user->display_name,
'course_name' => $course->post_title,
'issue_date' => date('F j, Y'),
'cert_id' => 'CERT-' . uniqid(),
]);
$response = wp_remote_get(add_query_arg([
'html' => $html,
'format' => 'pdf',
'pdf_landscape' => 'true',
'pdf_page_format' => 'A4',
'retina' => 'true',
'delay' => '500',
'cache_ttl' => '0',
], 'https://api.screenshotrun.com/v1/screenshots/capture'), [
'headers' => ['Authorization' => 'Bearer ' . SCREENSHOTRUN_KEY],
'timeout' => 30,
]);
save_and_email_certificate($user, wp_remote_retrieve_body($response));
});
Bulk generation for cohorts
Bootcamps and corporate training programs often graduate cohorts at once. Fifty or five hundred people finishing the same course on the same day. Sequential generation doesn't scale — 100 certificates at 2–3 seconds each runs for several minutes synchronously. Why wait three minutes when you can run them in parallel and finish in under 15 seconds?
For batches up to ~50, run requests in parallel with asyncio.gather() in Python or Promise.all() in Node.js:
import asyncio
import aiohttp
API_KEY = "YOUR_API_KEY"
BASE_URL = "https://api.screenshotrun.com/v1/screenshots/capture"
async def generate_certificate(session, learner):
html = render_template(learner)
params = {
"html": html, "format": "pdf",
"pdf_landscape": "true", "pdf_page_format": "A4",
"retina": "true", "delay": "500", "cache_ttl": "0",
}
async with session.get(BASE_URL, params=params,
headers={"Authorization": f"Bearer {API_KEY}"}) as resp:
content = await resp.read()
with open(f"certificates/{learner['id']}.pdf", "wb") as f:
f.write(content)
async def generate_cohort(learners):
async with aiohttp.ClientSession() as session:
await asyncio.gather(*[generate_certificate(session, l) for l in learners])
asyncio.run(generate_cohort(learners))
For larger cohorts, add webhook_url to the request instead of waiting for a synchronous response. The API accepts the request immediately and POSTs the finished PDF to your endpoint when rendering completes. Your handler returns 200 right away; certificates arrive in the background as they're ready. See webhook delivery for the callback payload format.
White-labeling for multi-tenant platforms
If you're building a SaaS platform that generates certificates on behalf of multiple client organizations, maintaining a separate HTML template per client doesn't scale. One base template, with client-specific styles injected at request time via the css parameter, handles it. How many template files do you actually need? One.
const clientBranding = {
acme_corp: { primary: '#1a2f5e', font: 'Inter', logo: 'https://cdn.example.com/acme-logo.svg' },
techcademy: { primary: '#2d7a4f', font: 'Poppins', logo: 'https://cdn.example.com/tech-logo.svg' },
};
const brand = clientBranding[clientId];
const css = `
:root {
--brand-primary: ${brand.primary};
--brand-font: '${brand.font}', sans-serif;
}
.certificate-logo { content: url('${brand.logo}'); }
`;
const params = { html: baseTemplate, css, format: 'pdf', retina: true, cache_ttl: 0 };
The css parameter injects a style block before capture. Your base template uses CSS custom properties for brand colors, font family, and logo placement. Each client gets a correctly branded certificate without a separate template file per organization.
Adding a tamper-evident audit layer
Who generated the certificate, and when? And can you prove it? If you're issuing certificates for compliance training — GDPR awareness, safety courses, regulatory onboarding — having a verifiable record of when each certificate was rendered can matter.
Adding certified=true to any capture request returns a SHA-256 hash and ISO 8601 timestamp alongside the output. The hash proves the certificate was generated with specific inputs at a specific time. It doesn't replace a full verification portal (that's app-layer logic, not a rendering API), but it adds an auditable layer for teams that need to demonstrate when and how a certificate was produced. The certified screenshot page covers what the certification object contains.
Limitations worth knowing
- Verification portals: The API generates the certificate image or PDF. It doesn't host a verification URL. If you want a QR code linking to a verification page, embed it in your HTML template pointing to your own endpoint. The API renders whatever QR code the template contains.
- Open Badges / Verifiable Credentials: Not supported. W3C Verifiable Credentials and Credly-style badge wallets require a credential management platform. This is a rendering API, not a credential issuer.
- Self-hosted fonts with authentication: Fonts fetched from public URLs load normally. Fonts behind authentication need to be base64-encoded inline in the HTML
<style>block, or served from a publicly accessible URL. - Multi-page certificates: Most designs fit on one landscape page. If yours exceeds the viewport height, add
full_page=true. For PDFs, page breaks are controlled by your CSS print rules (@media print,page-break-before).
The setup usually takes one afternoon. Write the certificate template in HTML — and in 2026, generating clean HTML from a design brief is a five-minute job for any LLM — add the API call to your course completion handler, and every learner from that point forward gets a branded PDF and a LinkedIn-ready PNG without anyone touching a design tool. The HTML part was never really the hard part. Rendering hundreds of those templates into print-quality PDFs reliably, without running your own browser infrastructure, is where teams consistently get stuck. That's what the API handles.
FAQ#
Yes. Web fonts loaded via <link> tags (Google Fonts, Adobe Fonts, self-hosted) load automatically before the capture. Add delay=500 to the request to give fonts time to load — increase to 1000ms if certificates render with fallback fonts. For fonts behind authentication, base64-encode the font file and embed it directly in a @font-face rule in your HTML <style> block.
A4 landscape at 96dpi is 1123×794px. Set width=1123 in the API call, or use pdf_page_format=A4 with pdf_landscape=true for PDF output. Add retina=true to double the output to 2246×1588px for print-quality rendering. For US Letter landscape, use 1056×816px.
For batches under ~50, run requests in parallel using Promise.all() in Node.js or asyncio.gather() in Python. For larger cohorts, add webhook_url to each request — the API accepts immediately and POSTs the finished PDF to your endpoint when rendering completes. This decouples generation from delivery and avoids synchronous timeouts on large batches.
Generate both. Use format=pdf with pdf_landscape=true and pdf_page_format=A4 for the downloadable/printable certificate. Use format=png with retina=true for the LinkedIn share image and email preview. Both calls use the same HTML template — run them in parallel to minimize total generation time.
Yes, but the QR code needs to be part of your HTML template. Generate the QR code server-side (pointing to your verification endpoint with the certificate ID) and embed it as an <img> tag in the template before passing it to the API. If the QR code is generated by client-side JavaScript, add wait_for_selector=".qr-code" to ensure it renders before capture.