HandlebarsPDF
Teilnahmebescheinigung mit Inhalten und Prüfcode
Eine Bescheinigung, die aufführt, was vermittelt wurde: die Module mit ihren Unterrichtseinheiten, die Summe als Zahl und von numToWords auf Deutsch ausgeschrieben, dazu ein QR-Code zum Prüfen der Bescheinigungsnummer. Randloses A4 ohne Ränder, für einen ganzen Kurs als ein Batch gerendert.
Kostenloser Plan, keine Kreditkarte. Sie erhalten eine eigene Kopie im Editor, veröffentlicht und bereit für die API.
Gerendertes Ergebnis
Was dieses Beispiel zeigt
{{#each course.modules}}führt auf, was vermittelt wurde;sum course.modules "units"addiert die Einheiten zu 40.numToWords (sum …) "de"schreibt die Summe als „vierzig“ aus, wie es bei solchen Bescheinigungen üblich ist.{{qrcode certificate.verify_url size=84}}kodiert den Prüf-Link neben der Bescheinigungsnummer.- Keine Ränder und eine Box von genau
297mmHöhe lassen den farbigen Balken bis an die Papierkante laufen;margin-top: autohält die Unterschrift unten. - Beide Daten bilden einen Zeitraum:
d. MMMMfür den ersten Tag,d. MMMM yyyyfür den letzten.
Verwendete Helfer
Vorlagenordner
<main class="sheet">
<p class="eyebrow">Teilnahmebescheinigung</p>
<h1>{{participant.name}}</h1>
<p class="lead">
hat vom {{date course.from "d. MMMM"}} bis {{date course.until "d. MMMM yyyy"}} an der Weiterbildung
<strong>{{course.title}}</strong> teilgenommen.
</p>
<table>
<thead><tr><th>Inhalte</th><th class="num">Unterrichtseinheiten</th></tr></thead>
<tbody>
{{#each course.modules}}
<tr><td>{{title}}</td><td class="num">{{number units 0}}</td></tr>
{{/each}}
</tbody>
<tfoot>
<tr><td>Gesamt ({{numToWords (sum course.modules "units") "de"}} Unterrichtseinheiten zu je 45 Minuten)</td><td class="num">{{number (sum course.modules "units") 0}}</td></tr>
</tfoot>
</table>
<footer>
<div class="signature"><span></span>{{course.instructor}}<br><small>Kursleitung, {{provider.name}}</small></div>
<figure>
<img src="{{qrcode certificate.verify_url size=84}}" alt="Prüfcode">
<figcaption>Nr. {{certificate.id}}<br>{{provider.city}}, {{date certificate.issued "d. MMMM yyyy"}}</figcaption>
</figure>
</footer>
</main>body { margin: 0; font-family: 'Inter', sans-serif; color: #0f172a; }
.sheet {
height: 297mm;
box-sizing: border-box;
display: flex;
flex-direction: column;
padding: 32mm 26mm 24mm;
border-top: 12mm solid #312e81;
}
.eyebrow { margin: 0; font-weight: 600; letter-spacing: 0.22em; text-transform: uppercase; color: #4f46e5; }
h1 { margin: 10mm 0 4mm; font-size: 32pt; }
.lead { margin: 0 0 12mm; font-size: 13pt; line-height: 1.55; color: #334155; }
table { width: 100%; border-collapse: collapse; font-size: 10.5pt; }
th { padding: 2mm 0; border-bottom: 1.5px solid #0f172a; text-align: left; font-size: 8pt; letter-spacing: 0.05em; text-transform: uppercase; color: #64748b; }
td { padding: 2.6mm 0; border-bottom: 1px solid #e2e8f0; }
tfoot td { border-top: 1.5px solid #0f172a; border-bottom: 0; font-weight: 600; }
.num { text-align: right !important; }
footer { margin-top: auto; display: flex; justify-content: space-between; align-items: flex-end; }
.signature { width: 72mm; font-size: 10pt; }
.signature span { display: block; margin-bottom: 2mm; border-top: 1px solid #0f172a; }
small, figcaption { font-size: 8pt; color: #64748b; }
figure { margin: 0; display: flex; align-items: flex-end; gap: 3mm; text-align: right; flex-direction: row-reverse; }<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap">{
"pdf": {
"metadata": {
"title": "Teilnahmebescheinigung {{participant.name}}"
}
},
"paper": {
"format": "A4"
},
"margin": {
"top": "0",
"right": "0",
"bottom": "0",
"left": "0"
},
"locale": "de-DE",
"timezone": "Europe/Berlin"
}{
"provider": {
"name": "Fennlor Akademie",
"city": "Musterstadt"
},
"participant": {
"name": "Max Mustermann"
},
"course": {
"title": "Dokumentenautomatisierung mit HTML und CSS",
"from": "2026-09-07",
"until": "2026-09-11",
"instructor": "Erika Mustermann",
"modules": [
{
"title": "Print-CSS: Seitenformate, Ränder, Umbrüche",
"units": 8
},
{
"title": "Vorlagensprachen: Jinja2, Liquid, Handlebars",
"units": 8
},
{
"title": "Rechnungen, GiroCode und E-Rechnung",
"units": 12
},
{
"title": "Tests, Versionen und Release Channels",
"units": 6
},
{
"title": "Praxisprojekt mit Abschlusspräsentation",
"units": 6
}
]
},
"certificate": {
"id": "TB-2026-0193",
"issued": "2026-09-11",
"verify_url": "https://akademie.example/pruefen/TB-2026-0193"
}
}Über die API rendern
Speichern Sie die Beispieldaten als data.json und senden Sie sie mit dem Namen der Vorlage. TypeScript und Python nutzen die SDKs; jede andere Sprache schickt dasselbe JSON mit ihrer Standardbibliothek an die REST-API. Renderings mit einem Schlüssel, der mit ff_test_ beginnt, werden nicht gezählt; im Free-Tarif tragen sie ein kleines Wasserzeichen.
jq '{template: "teilnahmebescheinigung", data: ., filename: "bescheinigung-max-mustermann.pdf"}' data.json \
| curl https://api-eu.formfeed.dev/v1/renders \
-H "Authorization: Bearer ff_test_..." \
-H "Content-Type: application/json" \
--data-binary @-import { Formfeed } from '@formfeed/sdk';
import data from './data.json' with { type: 'json' };
const formfeed = new Formfeed({ apiKey: process.env.FORMFEED_API_KEY! });
const render = await formfeed.renders.create({
template: 'teilnahmebescheinigung',
data,
filename: 'bescheinigung-max-mustermann.pdf',
});
console.log(render.status, render.download_url);import json
import os
from formfeed import Formfeed
formfeed = Formfeed(api_key=os.environ["FORMFEED_API_KEY"])
with open("data.json", encoding="utf-8") as file:
data = json.load(file)
render = formfeed.renders.create(
template="teilnahmebescheinigung",
data=data,
filename="bescheinigung-max-mustermann.pdf",
)
print(render.status, render.download_url)<?php
$body = [
'template' => 'teilnahmebescheinigung',
'data' => json_decode(file_get_contents('data.json'), true),
'filename' => 'bescheinigung-max-mustermann.pdf',
];
$curl = curl_init('https://api-eu.formfeed.dev/v1/renders');
curl_setopt_array($curl, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('FORMFEED_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($body),
]);
$render = json_decode(curl_exec($curl), true);
echo $render['status'], ' ', $render['download_url'], PHP_EOL;package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
data, err := os.ReadFile("data.json")
if err != nil {
panic(err)
}
body, _ := json.Marshal(map[string]any{
"template": "teilnahmebescheinigung",
"data": json.RawMessage(data),
"filename": "bescheinigung-max-mustermann.pdf",
})
request, _ := http.NewRequest("POST", "https://api-eu.formfeed.dev/v1/renders", bytes.NewReader(body))
request.Header.Set("Authorization", "Bearer "+os.Getenv("FORMFEED_API_KEY"))
request.Header.Set("Content-Type", "application/json")
response, err := http.DefaultClient.Do(request)
if err != nil {
panic(err)
}
defer response.Body.Close()
var render struct {
Status string `json:"status"`
DownloadURL string `json:"download_url"`
}
json.NewDecoder(response.Body).Decode(&render)
fmt.Println(render.Status, render.DownloadURL)
}using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
var body = new
{
template = "teilnahmebescheinigung",
data = JsonDocument.Parse(File.ReadAllText("data.json")).RootElement,
filename = "bescheinigung-max-mustermann.pdf",
};
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("FORMFEED_API_KEY"));
var response = await http.PostAsJsonAsync("https://api-eu.formfeed.dev/v1/renders", body);
var render = await response.Content.ReadFromJsonAsync<JsonElement>();
Console.WriteLine($"{render.GetProperty("status")} {render.GetProperty("download_url")}");import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
public class Render {
public static void main(String[] args) throws Exception {
String data = Files.readString(Path.of("data.json"));
String body = """
{
"template": "teilnahmebescheinigung",
"filename": "bescheinigung-max-mustermann.pdf",
"data": %s
}
""".formatted(data);
HttpRequest request = HttpRequest.newBuilder(URI.create("https://api-eu.formfeed.dev/v1/renders"))
.header("Authorization", "Bearer " + System.getenv("FORMFEED_API_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body()); // status, download_url, page_count, units …
}
}require "json"
require "net/http"
body = {
template: "teilnahmebescheinigung",
data: JSON.parse(File.read("data.json")),
filename: "bescheinigung-max-mustermann.pdf",
}
response = Net::HTTP.post(
URI("https://api-eu.formfeed.dev/v1/renders"),
JSON.generate(body),
"Authorization" => "Bearer #{ENV.fetch("FORMFEED_API_KEY")}",
"Content-Type" => "application/json"
)
render = JSON.parse(response.body)
puts render["status"], render["download_url"]Weitere Aufrufe
// everyone who attended, as one job with a zip at the end
const job = await formfeed.renders.batch({
template: 'teilnahmebescheinigung',
zip: true,
items: participants.map((participant) => ({
data: { provider, course, participant, certificate: certificateFor(participant) },
filename: `bescheinigung-${participant.name}.pdf`,
})),
});Heute mit dem Rendern beginnen
Der kostenlose Plan enthält 100 Einheiten im Monat und kostenlose Testrenderings. Wechseln Sie den Plan, wenn Sie mehr brauchen.
