Zum Inhalt springen

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

Gerendertes Ergebnis: Teilnahmebescheinigung mit Inhalten und Prüfcode
PDF · 1 Seite · 101 KB ↓ herunterladen

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 297mm Höhe lassen den farbigen Balken bis an die Papierkante laufen; margin-top: auto hält die Unterschrift unten.
  • Beide Daten bilden einen Zeitraum: d. MMMM für den ersten Tag, d. MMMM yyyy für den letzten.

Verwendete Helfer

Vorlagenordner

templates/teilnahmebescheinigung/template.htmlHandlebars
<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>
templates/teilnahmebescheinigung/style.cssCSS
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; }
templates/teilnahmebescheinigung/head.htmlHTML
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap">
templates/teilnahmebescheinigung/settings.jsonJSON
{
  "pdf": {
    "metadata": {
      "title": "Teilnahmebescheinigung {{participant.name}}"
    }
  },
  "paper": {
    "format": "A4"
  },
  "margin": {
    "top": "0",
    "right": "0",
    "bottom": "0",
    "left": "0"
  },
  "locale": "de-DE",
  "timezone": "Europe/Berlin"
}
templates/teilnahmebescheinigung/data/default.jsonJSON
{
  "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.

render.shShell
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 @-
render.tsTypeScript
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);
render.pyPython
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)
render.phpPHP
<?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;
main.goGo
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)
}
Program.csC#
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")}");
Render.javaJava
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 …
  }
}
render.rbRuby
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

TypeScript
// 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.