Anwendungsfall
Rechnungs-PDF-API
Formfeed ist eine API, die aus Rechnungsdaten das fertige PDF macht. Die Vorlage hält das Layout, die Rechnung selbst, die Formate und den rechtlichen Fußtext; Ihr Code sendet Kunde, Positionen und Rechnungsnummer als JSON. Dieselbe Vorlage kann eine E-Rechnung im Format ZUGFeRD bzw. Factur-X liefern, und eine geänderte Fußzeile ist eine neue Version der Vorlage, kein Deployment.
Kostenloser Plan mit 100 Einheiten im Monat, keine Kreditkarte
Summen, Daten und Währungen
money, number und date folgen Sprache, Zeitzone und Währung der Vorlage, und sum addiert die Positionen. Mit dem Parameter locale druckt dieselbe Vorlage 1.234,56 € für einen deutschen Kunden und €1,234.56 für einen irischen.
GiroCode auf der Rechnung
epcQr zeichnet den SEPA-Zahlungscode, den eine Banking-App scannt, aus Name, IBAN, Betrag und Verwendungszweck. qrcode und barcode decken Zahlungslinks und Artikelnummern ab.
Fußzeilen, die die Seiten zählen
Kopf- und Fußzeile wiederholen sich auf jeder Seite mit <span class="pageNumber"> von <span class="totalPages">, eine lange Positionstabelle läuft mit wiederholter Kopfzeile weiter, und pageBreak() beginnt dort eine neue Seite, wo Sie eine wollen.
E-Rechnungen aus derselben Vorlage
Eingeschaltet, ist das PDF eine ZUGFeRD- bzw. Factur-X-Rechnung: die Seite, die Ihr Kunde liest, mit der Rechnung darin als XML, vor der Auslieferung gegen die Regeln der EN 16931 geprüft. Ab dem Starter-Plan.
Keine Rechnung doppelt
Ein Idempotency-Key macht eine Wiederholung sicher: Sobald die erste Anfrage beantwortet ist, liefert derselbe Schlüssel 24 Stunden lang diese Antwort, und nichts wird noch einmal gerendert oder berechnet. Eine identische Anfrage mit einem Live-Schlüssel innerhalb von 24 Stunden wird kostenlos aus der gespeicherten Datei beantwortet; eine E-Rechnung entsteht immer neu.
Reproduzierbar
Jedes Rendering speichert die Vorlagenversion mit einer Prüfsumme davon, den Chromium-Build und einen SHA-256 der Datei: der spätere Beleg, welche Vorlage eine Rechnung erzeugt hat und dass die Datei nicht verändert wurde. Dieselben Eingaben mit demselben Chromium-Build ergeben dasselbe Dokument, aber nicht dieselben Bytes, denn die Datei trägt den Zeitpunkt ihrer Erstellung.
Ein Rechnungs-PDF aus JSON erzeugen
Eine Rechnung ist die Vorlage plus die Daten einer Rechnung. Speichern Sie die Daten als data.json und senden Sie sie mit dem Namen der Vorlage; die Antwort enthält den Link zum PDF. Die Vorlage addiert die Positionen und rechnet Umsatzsteuer und Gesamtbetrag selbst aus. TypeScript und Python nutzen die SDKs, jede andere Sprache schickt dasselbe JSON mit ihrer Standardbibliothek. Ein Schlüssel, der mit ff_test_ beginnt, rendert, ohne dass gezählt wird.
{
"company": {
"name": "Fennlor Studio GmbH",
"street": "Musterstraße 1",
"city": "12345 Musterstadt",
"iban": "DE36000000000000000000",
"bic": "XXXXDEXXXXX",
"vat_id": "DE000000000"
},
"customer": {
"name": "Olvarest GmbH",
"street": "Beispielweg 2",
"city": "54321 Beispielstadt"
},
"invoice": {
"number": "2026-0042",
"date": "2026-09-10",
"vat_rate": 19,
"lines": [
{
"description": "Consulting",
"qty": 8,
"price": 120,
"total": 960
},
{
"description": "Implementation",
"qty": 20,
"price": 110,
"total": 2200
}
]
}
}jq '{template: "invoice-de", data: .}' 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: 'invoice-de',
data,
});
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="invoice-de",
data=data,
)
print(render.status, render.download_url)<?php
$body = [
'template' => 'invoice-de',
'data' => json_decode(file_get_contents('data.json'), true),
];
$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": "invoice-de",
"data": json.RawMessage(data),
})
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 = "invoice-de",
data = JsonDocument.Parse(File.ReadAllText("data.json")).RootElement,
};
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": "invoice-de",
"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: "invoice-de",
data: JSON.parse(File.read("data.json")),
}
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"]Vollständige Anfrage- und Antwortformate in der Doku. Vollständiges Vorlagenbeispiel →
Das kommt heraus
Ein echtes Rendering der Beispielvorlage für diesen Anwendungsfall, direkt aus dem Render-Worker. Ein Klick öffnet die Datei.
Kostenloser Plan, keine Kreditkarte. Sie erhalten eine eigene Kopie im Editor, veröffentlicht und bereit für die API.
Eine Vorlage für jedes Land, in das Sie Rechnungen stellen
Eine Rechnung für einen deutschen Kunden lautet auf 3.760,40 €, dieselbe Rechnung für einen irischen auf €3,760.40. Die Helfer nehmen ihr Format aus Sprache, Zeitzone und Währung der Vorlage, und locale in der Anfrage schaltet sie für ein Dokument um. Der Wortlaut wechselt mit, wenn die Vorlage ihre Texte mit t aus einem Wörterbuch nimmt: Das mehrsprachige Beispiel ist eine Vorlage in zwei Sprachen.
{
"template": "invoice-de",
"data": { … },
"locale": "en-IE"
}Der Zahlungscode, den Ihre Kunden scannen
Ein GiroCode ist der EPC-QR-Code einer SEPA-Überweisung: Name, IBAN, Betrag und Verwendungszweck in einem Code, aus dem eine Banking-App eine ausgefüllte Überweisung macht. epcQr baut ihn aus den Daten der Rechnung, der Betrag im Code ist also der Betrag auf der Seite.
<img src="{{ epcQr({ name: company.name, iban: company.iban, bic: company.bic, amount: net + vat, text: 'Rechnung ' ~ invoice.number }, { size: 110 }) }}" alt="GiroCode">Der GiroCode-Generator erzeugt einen einzelnen Code im Browser, und GiroCode auf der Rechnung geht den Standard Feld für Feld durch und sagt, warum Banking-Apps einen Code ablehnen.
Die E-Rechnung: ZUGFeRD und Factur-X
Ist die E-Rechnung in der Vorlage eingeschaltet, ist jedes PDF, das sie rendert, eine ZUGFeRD- bzw. Factur-X-Rechnung: eine PDF/A-3, die die Rechnung als XML trägt (factur-x.xml), im Profil EN 16931 oder Basic. Das XML entsteht aus einem Block _invoice der Daten und wird gegen sein Schema und die Regeln der EN 16931 geprüft, bevor die Datei ausgeliefert wird. Eine Datei, die nicht besteht, wird nicht ausgeliefert, und der Bericht der Prüfung kommt mit jeder, die es wird.
Eine E-Rechnung kostet eine Einheit zusätzlich zu ihrem Rendering und setzt den Starter-Plan voraus; im Free-Plan erzeugen Test-Renderings bis zu 20 am Tag. XRechnung und UBL gibt es nicht. Wann eine E-Rechnung Pflicht ist und was bei Ihnen bleibt, steht auf der Seite zur E-Rechnung, und das E-Rechnungs-Beispiel ist eine vollständige Vorlage mit ihren Daten. Dieselbe Datei kann zusätzlich ein getaggtes PDF/UA-1-Dokument sein, von veraPDF gegen die Regeln dieses Standards geprüft, die eine Maschine prüfen kann: siehe barrierefreie PDFs.
Die Rechnungen eines Monats als ein Job
Ein Abrechnungslauf ist ein Batch: bis zu 1.000 Rechnungen in einer Anfrage, im Hintergrund gerendert, auf Wunsch mit einem Zip am Ende und einem Webhook job.completed, wenn der Job fertig ist. Senden Sie den Batch mit einem Idempotency-Key, und ein Lauf, der zweimal gestartet wird, stellt seine Rechnungen einmal in die Warteschlange. Jede Rechnung ist ein eigenes Rendering mit eigenem Link, und meta trägt Ihre Auftragsnummer bis in den Webhook.
Aus einer Tabelle oder einem CRM, ohne Code
Eine Rechnung muss nicht in Ihrem Code beginnen. Eine Zeile kommt in eine Tabelle, ein Geschäft wird abgeschlossen, eine Bestellung bezahlt: In einem Werkzeug, das eine Automation starten kann, macht der Formfeed-Schritt aus diesem Ereignis die Rechnung, und der nächste Schritt legt sie ab oder verschickt sie.
Formfeed steht im App-Verzeichnis von Zapier. Die Aktion listet Ihre veröffentlichten Vorlagen und zeigt die Felder der gewählten mit Namen. Die Dokumentation baut einen solchen Zap Schritt für Schritt: Aus einer Tabellenzeile wird eine Rechnung in Google Drive. Wie es um Make, n8n und Power Automate steht, sagt die Integrationsseite.
Was eine Rechnung kostet
Eine Rechnung mit bis zu fünf Seiten ist eine Einheit, jede weiteren fünf Seiten kosten eine mehr, und eine E-Rechnung kostet eine zusätzlich. Der Free-Plan hat 100 Einheiten im Monat und verlangt keine Kreditkarte. Starter hat 3.000 Einheiten für 19 € im Monat, rund 0,6 Cent pro Rechnung. Renderings mit einem Test-Schlüssel kosten nichts, solange Sie bauen, und ein fehlgeschlagenes Rendering wird nie gezählt. Alle Pläne stehen auf der Preisseite.
Was bei Ihnen bleibt
Formfeed druckt die Rechnung, die Ihre Daten beschreiben. Es vergibt keine Rechnungsnummern, es entscheidet nicht, welcher Umsatzsteuersatz für einen Umsatz gilt, es verschickt die Rechnung nicht, und es ist nicht das Archiv, das das Gesetz von Ihnen verlangt: Nummerierung, steuerliche Behandlung, Versand und Archiv bleiben bei Ihnen. Dateien bleiben für die Aufbewahrung Ihres Plans erhalten, einen Tag im Free-Plan, 30 Tage bei Starter und 365 Tage bei Pro; speichern Sie also jede Rechnung selbst. Ab dem Pro-Plan geht von jeder Datei beim Rendern eine Kopie in Ihren eigenen S3- oder R2-Bucket.
FAQ
- Kann ich meine bestehende HTML-Rechnungsvorlage weiterverwenden?
- Ja. HTML und CSS in den Editor einfügen oder mit der CLI pushen; Jinja2, Liquid und Handlebars werden unterstützt. Vorlagen eines anderen Anbieters importieren Sie mit einem Prüfbericht.
- Berechnet die API Summen und Umsatzsteuer?
- Die Vorlage tut es, aus Ihren Daten: Helfer wie
sum,multiplyundroundaddieren die Positionen und wenden den Satz an, den Sie senden. Welcher Satz für einen Umsatz gilt, entscheiden Sie, nicht die Vorlage. - Erzeugt sie ZUGFeRD, Factur-X oder XRechnung?
- ZUGFeRD und Factur-X, in den Profilen EN 16931 und Basic, ab dem Starter-Plan. XRechnung und UBL gibt es nicht.
- Wie vermeide ich, dass eine Rechnung doppelt entsteht?
- Senden Sie einen
Idempotency-Key-Header, der an Ihren eigenen Datensatz gebunden ist, etwa die Auftragsnummer. Sobald die erste Anfrage beantwortet ist, liefert eine Wiederholung mit demselben Schlüssel 24 Stunden lang diese Antwort, und nichts wird noch einmal gerendert oder berechnet. Eine Wiederholung, die kommt, während die erste Anfrage noch läuft, wird nicht erkannt, und eine Antwort, die die Datei selbst war (?output=binary), wird nicht gespeichert: Einzelheiten unter Idempotenz. - Wo liegen die PDFs?
- In der EU: Sie werden in Frankfurt gerendert und in der EU gespeichert, für die Aufbewahrungsdauer Ihres Plans oder die kürzere Frist, die Sie pro Anfrage mit
expires_insetzen. Download-Links sind signiert; Sie können die Bytes auch sofort abholen und selbst speichern. - Gibt es ein SDK?
- Ja:
@formfeed/sdkfür TypeScript und JavaScript undformfeedfür Python. Jede andere Sprache sendet dasselbe JSON über HTTPS; die Guides enthalten vollständige Programme für PHP, Go, C#, Java, Ruby und curl.
