A learner finishes your course and expects two things within a minute: a PDF they can print and frame, and an image they can post to LinkedIn. Both should come from one design, so the printed copy and the shared copy never disagree. Build the certificate as one HTML page in your app, then ask RenderScreenshot for a landscape PDF with zero margins and a PNG at the same aspect ratio. This page gives the exact requests, the template rules that keep the certificate on one sheet, and the batch pattern for a whole cohort.
It is one of our use cases for document output. For paper sizes, page numbers and print CSS in general, start at the HTML to PDF API page.
Five requests for one certificate
Each row is a complete POST body for https://api.renderscreenshot.com/v1/screenshot. The URL is the certificate's own page in your app, for example the public verification page that employers check.
| Output | Why | Body |
|---|---|---|
| A4 landscape PDF, edge to edge | Print and archive copy | {"url": "https://learn.example.com/c/7F3K-92QD", "output": {"format": "pdf", "filename": "certificate-7F3K-92QD"}, "pdf": {"paper": "a4", "landscape": true, "margin": "0"}} |
| US Letter landscape PDF | Learners in the US and Canada | {"url": "...", "output": {"format": "pdf"}, "pdf": {"paper": "letter", "landscape": true, "margin": "0"}} |
| Exact sheet size PDF | Template drawn at a size that is not a standard paper | {"url": "...", "output": {"format": "pdf"}, "pdf": {"width": "297mm", "height": "210mm", "margin": "0"}} |
| PNG for sharing, A4 ratio | LinkedIn post, email preview | {"url": "...", "viewport": {"width": 1600, "height": 1131}, "output": {"format": "png"}} |
| Small card for the verify page | og:image of the verification URL |
{"url": "...", "preset": "og_card"} |
print_background is true by default, so the cream fill and the colored border print without extra settings. Every option is in the PDF parameters reference.
We checked the paper rules on this page in Chromium through Playwright 1.64.1 on 2026-10-18, with a 297 x 210 mm certificate template. We did not call the hosted API for this page, so there are no render times or file sizes here.
How it works
flowchart LR
A[Learner completes course] --> B[Your app creates the certificate record]
B --> C[Public page /c/:code renders the certificate HTML]
B --> D[Background job]
D -->|POST /v1/screenshot, format pdf| E[RenderScreenshot: Chrome loads /c/:code]
D -->|POST /v1/batch, format png| E
E --> F[PDF and PNG bytes + X-Cache-URL]
F --> G[Your bucket or database]
G --> H[Email to learner, download button, share link]
The verification page does two jobs. People open it to check that a certificate is real, and the renderer loads it to make the files. One template means one place to fix a typo in a course name.
Keep the files after the render. The X-Cache-URL header is a public CDN link that lives for the cache TTL: 24 hours by default and 30 days at most (caching). A certificate must outlive that. On paid plans, custom storage copies each render to your own bucket. Its storage.path template must contain {hash} and {ext}.
The template rules that matter
Certificates fail in a few predictable ways. These rules come from our Chromium test of the template above.
Size the sheet to the paper, then set the margin to zero. Our template draws a 297mm by 210mm sheet. On A4 landscape with margin: 0, it filled exactly one page. On Letter landscape, which is 279 mm wide, Chrome scaled the sheet down to fit the width, so the result was still one page but with a band of empty paper. If you serve both paper sizes, send pdf.width and pdf.height equal to the sheet, or keep one template per paper size.
Expect @page to win. The template has @page { size: A4 landscape; margin: 0; }. When we also passed a 1 cm margin to Chrome, the PDF still had no margin: the CSS rule overrode it. That is useful for certificates, because the template controls its own bleed. It also explains the opposite surprise: a stylesheet with an @page margin adds a white frame that no API option removes. The CSS paged media entry covers the rules.
Fix the name size on the server. "Maria-Fernanda Oyelaran-Castellanos" needs a smaller font than "Li Wei". Choose the size from the string length when you render the HTML, and put it in an inline custom property:
<div class="name" style="--name-size: 34pt">Maria-Fernanda Oyelaran-Castellanos</div>
A client script that shrinks text until it fits works too, but then the renderer has to wait for it. With the size fixed on the server, the page is final at load and needs no wait options.
Escape the learner's name. The name is user input. ERB, Blade, Jinja and JSX escape it by default; a raw helper (html_safe, {!! !!}, dangerouslySetInnerHTML) does not. A name such as <img src=x onerror=...> would otherwise run in the renderer.
Load fonts from your own origin. A script font that arrives late prints in the fallback serif. Self-host the font files next to the template and give them font-display: block. Our font consistency post has the details.
A cohort in two calls
When 300 learners finish on the same day, render the share images in batches and the PDFs one by one in a queue:
// issue-certificates.mjs: Node.js 20+, npm install renderscreenshot import { writeFile } from 'node:fs/promises'; import { Client, TakeOptions } from 'renderscreenshot'; const client = new Client(process.env.RS_API_KEY); const codes = ['7F3K-92QD', 'Q8ZP-41LM']; // from your database const pageUrl = (code) => `https://learn.example.com/c/${code}`; // 1. Share images: one batch request. PNG is the default format. const batch = await client.batch( codes.map((code) => ({ url: pageUrl(code), options: { width: 1600, height: 1131 } })) ); console.log(batch.status, batch.completed ?? '', 'of', batch.total); // 2. Print PDFs: one request each, because batch items take no pdf options. for (const code of codes) { const pdf = await client.take( TakeOptions.url(pageUrl(code)) .format('pdf') .pdfPaperSize('a4') .pdfLandscape() .pdfMargin('0') .pdfPrintBackground() ); await writeFile(`certificate-${code}.pdf`, pdf); }
A batch of more than 10 URLs runs asynchronously: it answers with a batch ID, and you poll it or receive a webhook (batch endpoint). How many items render at once depends on your plan. A batch checks that you have at least as many credits as URLs before it starts.
Settings, and why
| Setting | Value | Reason |
|---|---|---|
pdf.margin |
"0" |
The design has its own border; a page margin adds a white frame |
pdf.landscape |
true |
Certificates are wider than tall |
pdf.print_background |
true (default) |
Fills and borders are backgrounds |
viewport for PNG |
1600 x 1131 |
Same 1.414 ratio as A4, sharp enough for a feed |
output.filename |
certificate-<code> |
The name the browser offers on download |
cache.available |
true on re-downloads |
An identical request within the TTL returns the stored file for 0 credits |
What it costs
Every successful render is 1 credit, whatever the format or page count, and failed renders are free (credits and billing). One certificate as PDF plus PNG is 2 credits. A school that issues 1,000 certificates a month uses 2,000 credits, which fits the Starter plan (2,500 credits for $10 a month), or about $0.0050 per file. The free plan's 150 monthly credits cover a pilot cohort. Current prices are on the pricing page.
Where to go next
- Invoices need page numbers and tables that break cleanly; see invoice PDFs.
- Every PDF option, with types and limits: PDF parameters.
- Large cohorts and webhooks: bulk screenshots.
Get a free API key and render your first certificate from your own template.