Rendering
POST /v1/renders ist der Endpunkt, den Sie am häufigsten aufrufen. Er nimmt eine Quelle, Daten und Optionen entgegen und liefert ein Rendering mit Download-Link. Er braucht den Scope render:create. Die API-Referenz listet jedes Feld; diese Seite erklärt, wie sie zusammenspielen.
Die Felder der Anfrage im Überblick
| Feld | Typ | Standard | Bedeutung |
|---|---|---|---|
template | String | – | Eine gespeicherte Vorlage: ihr Slug, ihre tpl_…-ID oder UUID. Senden Sie genau eines von template, html und url (Quellen). |
html | String | – | Ad-hoc-HTML, höchstens 5 MB. |
url | String | – | Eine öffentliche https://-Seite, höchstens 2.048 Zeichen. |
engine | jinja2, liquid, handlebars oder null | null | Die Vorlagensprache von html. null druckt das HTML unverändert. |
version | String | published | Welche Version von template rendert: published, ein Channel-Name, latest (nur Test-Schlüssel) oder eine Versionsnummer (welche Version). |
data | Objekt | {} | Die Daten, mit denen die Vorlage gefüllt wird. |
output | pdf, png, jpg, webp, docx, pptx | das eigene Format der Quelle | Das Dateiformat (Ausgabeformat). |
settings | Objekt | – | Überschreibt die Einstellungen der Vorlage für diese Anfrage (Einstellungen überschreiben). |
locale | String | die der Vorlage | Kurzform für settings.locale, höchstens 20 Zeichen, etwa de-DE. |
validate_data | Boolean | false | data vor dem Rendern gegen das gespeicherte Schema der Vorlage prüfen (Daten prüfen). |
filename | String | document.<format> | Der Name der Ausgabedatei, höchstens 200 Zeichen (Dateiname). |
access | signed oder public | die Einstellung Ausgabe-URLs des Workspace, signed, wenn niemand sie geändert hat | Art des Download-Links (Download-Links). |
expires_in | Ganzzahl (Sekunden) | die Aufbewahrungsfrist | Die Ausgabe früher löschen, als die Aufbewahrungsfrist vorsieht. Kann sie nur verkürzen. |
mode | sync oder async | sync | Auf das Dokument warten oder sofort 202 erhalten (synchron oder asynchron). |
webhook_url | String | – | Eine öffentliche https://-Adresse, die die Ereignisse dieses Renderings zusätzlich erhält (Webhook-URLs pro Rendering). |
webhook_secret | String, 16–200 Zeichen | – | Signiert die Zustellungen an webhook_url; ohne ihn sind sie unsigniert. Braucht webhook_url und wird nie zurückgegeben. |
meta | Objekt | {} | Ihre eigenen Felder, zurückgegeben im Rendering und seinen Webhooks (Metadaten). |
dedupe | Boolean | true | false rendert auch dann, wenn ein identisches Rendering wiederverwendet werden könnte (Deduplizierung). |
canary_key | String, 1–200 Zeichen | – | Entscheidet, auf welcher Seite des Canary eines Channels ein Rendering landet (wer den Canary bekommt). |
post | Objekt | – | Schritte für Zusammenführen, Wasserzeichen, Passwort und E-Rechnung am PDF (Post-Processing). |
storage | false oder { "key": "…" } | – | Die Kopie im eigenen Bucket des Workspace. |
region | eu, us oder apac | die Region des Workspace | Lassen Sie es weg. Heute rendert nur die EU-Region (Datenresidenz). |
Quelle wählen
Jede Anfrage enthält genau eines der Felder template, html oder url; keines oder zwei ergeben 400 invalid_request.
- Eine gespeicherte Vorlage
- Ad-hoc-HTML
- Eine Webseite
Der Normalfall: die veröffentlichte Version der Vorlage plus Ihre Daten.
{
"template": "invoice",
"data": { "invoice": { "number": "2026-0042", "lines": [ … ] } }
}
template ist der Slug, die tpl_…-ID oder die UUID. Die API rendert die veröffentlichte Version, sofern version keine andere wählt; eine Vorlage ohne veröffentlichte Version antwortet mit 404 template_not_found. Die Vorlage sieht das Brand Kit der Organisation als brand und kann geteilte Partials einbinden.
HTML, das Sie selbst erzeugen, optional mit einer Vorlagensprache. Nützlich für einmalige Dokumente und zum Ausprobieren.
{
"html": "<h1>Hallo {{ name }}</h1>",
"engine": "liquid",
"data": { "name": "Jane Doe" },
"settings": { "paper": { "format": "A5" } }
}
Ohne engine (oder mit "engine": null) wird das HTML unverändert gedruckt. Bis zu 5 MB. Mit einer Engine funktionieren alle Helfer, das Brand Kit und geteilte Partials wie in einer gespeicherten Vorlage; die Schriften der Organisation und Übersetzungswörterbücher gehören zu gespeicherten Vorlagen und werden für html nicht geladen, binden Sie Schriften also mit eigenen @font-face-Regeln ein. Mit "tailwind": true in den Einstellungen werden die Tailwind-Klassen des HTML mit oder ohne Engine kompiliert; ein vollständiges Dokument (eines, das mit einem Doctype oder einem html-Tag beginnt) bekommt das Stylesheet an den Anfang seines Head, vor seine eigenen Styles.
Druckt eine öffentliche Webseite so, wie Chromium sie sieht.
{
"url": "https://reports.fennlor.example/2026-09",
"settings": { "paper": { "format": "A4", "landscape": true } }
}
Die Seite wird so geladen, wie ein anonymer Besucher sie sieht: Die Anfrage trägt keine Ihrer Cookies oder Header, eine Seite hinter einer Anmeldung rendert also ihre Anmeldemaske. Rufen Sie eine solche Seite selbst ab und rendern Sie sie als html.
- Sofort abgelehnt mit
400 invalid_request: eine Adresse mit mehr als 2.048 Zeichen oder mit Benutzername oder Passwort darin,localhost, private, Loopback-, Link-Local- und andere reservierte IP-Adressen (IPv4 und IPv6), numerische Kurzformen wie2130706433, Metadatendienste von Clouds und jeder Name unter.internal,.local,.localhostund.fly.dev. - Schlägt fehl mit
422 navigation_failed: einehttp://-Adresse (geladen werden nurhttps://-Seiten), ein Name, der sich nicht auflösen lässt oder auf eine private Adresse zeigt, eine Seite, die nicht innerhalb von 30 Sekunden lädt, und eine Seite, die mit einem HTTP-Status ab 400 antwortet. - Bilder, Schriften und Skripte, die die Seite von gesperrten Adressen lädt, werden weggelassen; die Seite rendert ohne sie.
- Ein
url-Rendering wird nie dedupliziert: Jede Anfrage lädt die Seite neu.
Ausgabeformat
output ist pdf, png, jpg, webp, docx oder pptx. Welche Werte ein Rendering anfordern darf, hängt von seiner Quelle ab:
| Quelle | output | Ohne output |
|---|---|---|
Vorlage der Art pdf | pdf, png, jpg, webp | pdf |
Vorlage der Art image | pdf, png, jpg, webp | ihr settings.image.format, PNG, wenn keines gesetzt ist |
Word-Vorlage (Art docx) | docx, pdf | ihr settings.office.output, sonst docx |
PowerPoint-Vorlage (Art pptx) | pptx, pdf | ihr settings.office.output, sonst pptx |
Ad-hoc-html, url | pdf, png, jpg, webp | pdf |
Jede andere Kombination beantwortet die API mit 400 invalid_request und den erlaubten Werten in allowed. Bei Bildern kommt der Viewport aus settings.image (Referenz der Einstellungen); eine Vorlage der Art image bringt ihn bereits mit. Ohne Einstellungen ist ein Bild der Viewport von 1200 × 630 px mit dem Skalierungsfaktor 2, die Datei misst also 2400 × 1260 Pixel.
{
"template": "social-card",
"data": { "title": "Release 1.4" },
"output": "png"
}
Word- und PowerPoint-Vorlagen rendern mit einem Live-Schlüssel ab dem Starter-Plan; im Free-Plan beantwortet die API ein Live-Rendering mit 403 forbidden und feature: "office". Im Free-Plan erzeugen ihre Test-Renderings nur das PDF mit der Testmarkierung: Ein Test-Rendering, das docx oder pptx anfordert oder output weglässt, sodass das eigene Format der Vorlage gilt, beantwortet die API mit 403 test_output_unavailable. Senden Sie dort mit einem Test-Schlüssel "output": "pdf".
Ein PDF mit mehr Seiten als das Seitenlimit des Plans schlägt mit 422 page_limit_exceeded fehl, eine Datei über dem Größenlimit des Plans mit 422 output_too_large; siehe Plan-Limits. Beides kostet nichts.
Die Datei statt des JSON
POST /v1/renders?output=binary antwortet mit dem Dokument selbst: den Bytes, Content-Type und Content-Disposition: attachment mit dem Dateinamen, dazu ID, Seitenzahl und Ablauf des Renderings in X-Render-Id, X-Render-Pages und X-Render-Expires-At sowie den üblichen Nutzungs-Headern. Praktisch für ein Shell-Skript oder einen Proxy, der die Datei direkt weiterreicht:
curl -X POST "https://api-eu.formfeed.dev/v1/renders?output=binary" \
-H "Authorization: Bearer $FORMFEED_API_KEY" -H "Content-Type: application/json" \
-d '{ "template": "invoice-de", "data": { "invoice": { "number": "2026-0042" } } }' \
-o rechnung.pdf
Ein Rendering, das in den Hintergrund wandert, antwortet unabhängig vom Query-Parameter mit 202 und dem JSON-Rendering, weil es noch keine Datei gibt. Sein Location fragt erneut nach der Datei: GET /renders/{id}/result?output=binary antwortet mit 202, bis das Rendering fertig ist, und dann mit der Datei. Eine Antwort mit der Datei wird für idempotente Wiederholungen nicht gespeichert.
Welche Version rendert
Standardmäßig rendert die veröffentlichte Version. version nimmt auch eine Versionsnummer, und latest rendert die neueste Version, ob veröffentlicht oder nicht (meist den Entwurf, an dem der Editor arbeitet) – das darf nur ein Test-Schlüssel (ein Live-Schlüssel erhält 400 invalid_request). So probieren Sie eine Änderung vor der Veröffentlichung aus:
{
"template": "invoice-de",
"version": "latest",
"data": { "invoice": { "number": "2026-0042" } }
}
version nimmt auch den Namen eines Release Channels wie staging, den Live- und Test-Schlüssel rendern dürfen. Ein Channel ist ein Zeiger, den jemand bewusst auf eine Version verschiebt; eine Staging-Umgebung rendert so, was für sie freigegeben wurde, statt jedem Speichern im Editor zu folgen. Release Channels gibt es ab dem Starter-Plan; im Free-Plan ergibt ein Channel-Name 403 forbidden mit feature: "channels".
{
"template": "invoice-de",
"version": "staging",
"data": { "invoice": { "number": "2026-0042" } }
}
Die veröffentlichte Version und Channels werden bis zu einer Minute zwischengespeichert: Eine neu veröffentlichte Version oder ein verschobener Channel rendert innerhalb dieser Minute. Eine Versionsnummer oder latest wird nie zwischengespeichert. Ein Channel mit Canary schickt seinen Anteil der Renderings an die Canary-Version, gewählt anhand von canary_key, wenn die Anfrage einen sendet, sonst anhand des Idempotency-Key, sonst zufällig (wer den Canary bekommt). Ein Channel, den die Vorlage nicht hat, antwortet mit 404 channel_not_found, eine Versionsnummer, die sie nicht hat, mit 404 template_not_found.
Einstellungen pro Anfrage überschreiben
settings wird per Deep Merge über die Einstellungen der Vorlage gelegt, Sie senden also nur, was abweicht. null entfernt einen Abschnitt:
{ "template": "invoice", "data": { … }, "settings": { "paper": { "format": "Letter" }, "footer": null } }
locale ist eine Kurzform für settings.locale: Es stellt die Formatierung von Datum, Zahlen und Währungen sowie das Übersetzungswörterbuch für dieses eine Dokument um.
{ "template": "invoice", "data": { … }, "locale": "fr-FR" }
settings.pdf.ua macht die Datei zu einem PDF/UA-1-Dokument: einem getaggten PDF, das veraPDF vor der Auslieferung gegen die maschinell prüfbaren Regeln von PDF/UA-1 prüft. Das Rendering trägt dann das Urteil des Validators als accessibility.
{ "template": "notice", "data": { … }, "settings": { "pdf": { "ua": true } } }
Die Daten zuerst prüfen
Formfeed prüft Ihre Daten nur, wenn Sie darum bitten. Mit "validate_data": true wird data gegen das gespeicherte Datenschema der Vorlage geprüft, bevor etwas gerendert oder gezählt wird:
{
"template": "invoice",
"data": { "invoice": { "number": 42 } },
"validate_data": true
}
- Passen die Daten nicht, antwortet die API mit
422 data_validation_error;errorslistet jede Abweichung mit ihrempathund ihrermessage. Es wird nichts gerendert und nichts berechnet. - Eine Vorlage ohne gespeichertes Schema und eine
html- oderurl-Quelle ergeben400 invalid_request: Es gibt nichts, wogegen geprüft werden könnte. - Ohne
validate_datarendert ein fehlendes Feld einfach leer. Um eine Vorlage mit Daten zu prüfen, ohne zu rendern, nutzen SiePOST /templates/{id}/validate.
Dateiname
filename benennt die Ausgabedatei. Der Name steht am Ende der Download-URL und im Header Content-Disposition, Browser speichern die Datei also unter diesem Namen. Ein PDF oder eine Office-Datei wird heruntergeladen, statt im Tab geöffnet zu werden; ein Bild (PNG, JPG, WebP) öffnet sich im Tab, wo ein og:image oder ein angeklickter Link gezeigt wird, und wird unter seinem Namen gespeichert. Mit dem Abfrageparameter download=1 am Link eines Bildes wird es stattdessen heruntergeladen.
{
"template": "invoice",
"data": { "invoice": { "number": "2026-0042" } },
"filename": "rechnung-2026-0042.pdf"
}
Der Name wird verwendet, wie Sie ihn senden, nicht als Vorlage: Bauen Sie ihn in Ihrem Code zusammen. Zeichen außer den Buchstaben A–Z und a–z, Ziffern, _ . - ( ) und Leerzeichen werden zu _ (aus Rechnung März.pdf wird Rechnung M_rz.pdf, aus rechnung-{{ invoice.number }}.pdf wird rechnung-_ invoice.number _.pdf), der Name wird auf 150 Zeichen gekürzt, und die Endung der Ausgabe wird angehängt, wenn sie fehlt. Ohne filename heißt die Datei document.pdf, document.png und so weiter.
Download-Links, Zugriff und Aufbewahrung
Ein erfolgreiches Rendering hat eine download_url:
- Signierte Links (
"access": "signed") tragen ein Ablaufdatum und eine Signatur. Sie gelten höchstens sieben Tage und nie länger, als die Datei selbst aufbewahrt wird.GET /renders/{id}stellt jederzeit einen frischen Link aus. Speichern Sie also die ID des Renderings, nicht die URL. - Öffentliche Links (
"access": "public") haben keine Signatur und funktionieren, bis die Datei gelöscht wird. Verwenden Sie sie nur für Dokumente, die nicht vertraulich sind. - Ohne
accessgilt die Einstellung des Workspace unter Einstellungen → Ausgabe-URLs, die Signiert ist, sofern kein Admin sie geändert hat. - Aufbewahrung: Der Plan legt fest, wie lange Ausgaben aufbewahrt werden: 1 Tag im Free-Plan, 30 Tage im Starter-Plan, 365 Tage im Pro-Plan und ohne Limit im Business-Plan. Ein Admin kann die Frist für einen Workspace unter Einstellungen → Aufbewahrung der Ausgaben (Tage) verkürzen, und
expires_in(Sekunden) verkürzt sie für ein einzelnes Rendering. Verlängern lässt sie sich mit keinem von beiden: Eine längere Frist in den Einstellungen des Workspace gilt als die des Plans, und ein längeresexpires_inwird auf die Aufbewahrungsfrist gekürzt. Ausgaben von Test-Renderings werden spätestens nach 24 Stunden gelöscht, gleich welcher Plan. - Vorzeitiges Löschen:
DELETE /renders/{id}/outputsentfernt die Dateien sofort (Scopefile:delete). Die Metadaten des Renderings bleiben im Protokoll, mitdownload_url: null.
Wenn Sie Dokumente selbst archivieren, bewahren Sie sie in Ihrem eigenen Speicher auf. Verlassen Sie sich nicht auf Formfeed als Archiv. Eigener Speicher (ab dem Pro-Plan) kopiert jedes Dokument beim Rendern in Ihren Bucket bei Amazon S3, Cloudflare R2 oder einem S3-kompatiblen Dienst; ohne ihn laden Sie jede Datei einmal herunter und legen sie selbst ab.
Synchron oder asynchron
Standardmäßig wartet die Anfrage auf das Dokument (synchron) und antwortet mit 200 und dem fertigen Rendering.
Setzen Sie "mode": "async", um sofort 202 Accepted mit dem Rendering im Status queued zu erhalten. Das Ergebnis erfahren Sie dann über einen Webhook oder durch Abfragen von GET /renders/{id}. Verwenden Sie den asynchronen Modus für lange Dokumente, für alles, was eine Warteschlange speist, und immer dann, wenn der Aufrufer nicht warten soll.
Eine synchrone Anfrage wartet höchstens eine Minute. Zuerst braucht sie einen freien Render-Slot: Sind alle Slots belegt, wartet sie bis zu 30 Sekunden auf einen und geht dann in die Warteschlange. Eine synchrone Anfrage, die innerhalb von 30 Sekunden keinen Slot gefunden hat oder nach 60 Sekunden keine Antwort hat, wird nicht abgebrochen. Sie wird zu einem asynchronen Rendering: Die Antwort ist 202 mit X-Render-Async-Fallback: true, einem Location-Header und Retry-After: 5, und das Rendering wird im Hintergrund fertig. Ihr Code sollte daher auch dann mit einem 202 umgehen können, wenn er synchron angefragt hat: Folgen Sie Location, oder rufen Sie waitFor der SDKs (wait_for in Python) mit der ID des Renderings auf. Siehe der automatische Fallback.
Mehr dazu, einschließlich Batches mit bis zu 1.000 Dokumenten, unter Asynchron und Batch.
Wie lange ein Rendering dauern darf
| Schritt | Limit | Wenn es überschritten wird |
|---|---|---|
| Die Vorlage mit den Daten füllen | 5 Sekunden, höchstens 20 MB HTML | 504 render_timeout, bei der Größe 422 output_too_large |
Das Dokument in Chromium laden, mit allem, was es lädt, bis keine seiner Anfragen mehr läuft (wie, sagt waitFor.networkIdle) | 30 Sekunden | 504 render_timeout; bei einem url-Rendering 422 navigation_failed |
Auf settings.waitFor.selector warten | waitFor.timeoutMs, standardmäßig 5 Sekunden, höchstens 30 | 504 render_timeout |
settings.waitFor.delayMs | höchstens 10 Sekunden | ein längerer Wert wird auf 10 Sekunden gekürzt |
| Das PDF drucken | 120 Sekunden | 504 render_timeout |
Diese Limits gelten pro Rendering, nicht pro Anfrage: Eine synchrone Anfrage, die insgesamt länger als 60 Sekunden läuft, wandert in den Hintergrund, und das Rendering läuft weiter. Die waitFor-Einstellungen beschreibt Auf Inhalte warten.
Post-Processing
post führt PDF-Schritte am fertigen Dokument aus, als Teil desselben Renderings: andere PDFs anhängen, ein Wasserzeichen aufbringen, ein Passwort setzen, eine E-Rechnung erzeugen.
{
"template": "contract",
"data": { … },
"post": {
"merge_after": ["rnd_0f3c9a2e7b1d4e5f8a6c3b2d1e0f9a8b", "terms.pdf"],
"watermark": { "text": "DRAFT", "opacity": 0.15 },
"password": { "user": "open-sesame", "owner": "only-for-us", "permissions": ["print"] }
}
}
| Schritt | Optionen |
|---|---|
merge_after | 1 bis 49 PDFs, die in dieser Reihenfolge hinter dieses Dokument gehängt werden: IDs (rnd_…) erfolgreicher PDF-Renderings im selben Workspace, deren Datei noch existiert, oder PDFs aus der Dateibibliothek über ihren Namen (Quellen). Die angehängten Seiten werden nicht noch einmal berechnet, page_count zählt sie aber mit. |
watermark | text (1–200 Zeichen) oder image (ein PNG oder JPEG aus der Dateibibliothek, über seinen Namen), opacity (0–1, Standard 0.2), rotation (Grad im Uhrzeigersinn, −360 bis 360; Standard -45 für Text, von unten links nach oben rechts, und 0 für ein Bild), color (Hex wie #cc0000, nur für Text). Text wird in Noto Sans Bold gesetzt, die in die Datei eingebettet wird und lateinische, griechische und kyrillische Buchstaben abdeckt; ein Text mit anderen Zeichen wird mit 400 invalid_request abgelehnt. Ein Bild wird so skaliert, dass es in 60 % der Seite passt. |
password | user (zum Öffnen nötig, 1–127 Zeichen), owner (zum Aufheben der Einschränkungen nötig, 1–127 Zeichen; fehlt es, wird ein zufälliges gesetzt, sodass niemand sie aufheben kann), permissions: was ein Leser ohne Owner-Passwort darf, aus print, copy, modify, annotate. Fehlt es, sind alle vier erlaubt; [] erlaubt nichts. Senden Sie mindestens eines der drei. AES-256. |
einvoice | Macht aus dem PDF eine ZUGFeRD- oder Factur-X-E-Rechnung, gebaut aus dem _invoice-Block in data: profile, flavour, xml, display_check, alle optional, oder false, um abzuschalten, was die Vorlage festlegt. |
- Die Schritte laufen in der Reihenfolge Zusammenführen, Wasserzeichen, Passwort, egal in welcher Reihenfolge Sie sie schreiben.
- Zusammenführen, Wasserzeichen und Passwort kosten mit Live-Schlüsseln je 0,5 Einheiten, mit Test-Schlüsseln nichts. Sie setzen den Starter-Plan oder höher voraus; im Free-Plan antwortet die API mit
403 forbiddenundfeature: "post_processing". einvoiceist ein eigener Schritt: Er läuft zuletzt, kostet mit Live-Schlüsseln 1 Einheit und lässt sich nicht mitpasswordkombinieren (PDF/A verbietet Verschlüsselung). Live-E-Rechnungen setzen den Starter-Plan voraus (im Free-Plan403mitfeature: "einvoice"); Test-Renderings erzeugen in jedem Plan eine begrenzte Zahl pro Tag, immer mit der Testmarkierung (Pläne und Einheiten). Mitsettings.pdf.uaist dieselbe Datei zugleich ein PDF/UA-1-Dokument, und das Rendering trägt beide Berichte.- Post-Processing funktioniert nur bei PDF-Ausgabe; jedes andere
outputergibt400 invalid_request. Unbekannte Schlüssel inpostwerden mit400abgelehnt, damit ein Tippfehler nie stillschweigend ein ungeschütztes PDF erzeugt. - Nachbearbeitete Renderings werden nicht dedupliziert.
Um Dokumente zu bearbeiten, die Sie bereits gerendert haben, nutzen Sie die Endpunkte der PDF-Tools: POST /v1/pdf/merge, /pdf/protect und /pdf/watermark liefern ein neues Rendering, und POST /v1/pdf/info liefert kostenlos Seitenzahl, Seitengrößen und Metadaten eines PDF-Renderings. Siehe PDF-Tools.
Metadaten
meta ist ein frei gestaltbares Objekt, das Formfeed mit dem Rendering speichert und in der Antwort, in GET /renders/{id} und in jedem Webhook zu diesem Rendering zurückgibt. Legen Sie dort Ihre eigenen Kennungen ab, damit ein Webhook-Handler weiß, zu welchem Datensatz das Dokument gehört:
{ "template": "invoice", "data": { … }, "mode": "async", "meta": { "order_id": "8812", "tenant": "fennlor" } }
Deduplizierung
Ein Live-Rendering wird aus einem früheren beantwortet, statt erneut zu rendern, wenn derselbe Workspace in den letzten 24 Stunden dasselbe gerendert hat und diese Datei noch existiert. „Dasselbe“ heißt: dieselbe Vorlagenversion (oder dasselbe html), Engine, Daten, Ausgabe, Einstellungen, Locale, Region und derselbe Zugriff, mit denselben Versionen von Brand Kit und geteilten Partials. Eine solche Antwort kostet 0 Einheiten, kommt in Millisekunden an und ist mit "deduplicated": true und dem Header X-Render-Deduplicated: true markiert.
Die Antwort ist das frühere Rendering: seine id, seine Datei und damit sein Dateiname und sein expires_at, mit dem meta Ihrer Anfrage und template: null. Im Render-Protokoll erscheint kein neuer Eintrag. filename, expires_in, meta und mode werden nicht verglichen; eine Anfrage, die sich nur darin unterscheidet, erhält also die frühere Datei unter ihrem früheren Namen.
Das ist meist gewünscht: Wer zweimal auf „Rechnung herunterladen“ klickt, erhält kostenlos dieselbe Datei. Senden Sie "dedupe": false, wenn jede Anfrage eine neue Datei erzeugen muss. Nie dedupliziert werden: Test-Renderings, url-Quellen, Renderings mit post (E-Rechnungen eingeschlossen), Einträge eines Batches und ein Rendering, das sein eigenes Objekt im Bucket des Workspace nennt (storage.key, siehe Eigener Speicher).
Deduplizierung ist nicht dasselbe wie Idempotenz: Die Deduplizierung vergleicht den Inhalt zweier Anfragen, die Idempotenz den Schlüssel, den Sie senden. Verwenden Sie Idempotenz-Schlüssel in jedem Fall, damit Wiederholungen sicher sind.
Das Rendering-Objekt
{
"id": "rnd_0f3c9a2e7b1d4e5f8a6c3b2d1e0f9a8b",
"status": "succeeded",
"output": "pdf",
"download_url": "https://cdn.formfeed.dev/o/eu/…/invoice-2026-0042.pdf?exp=…&sig=…",
"expires_at": "2026-09-18T10:00:01.000Z",
"bytes": 48213,
"page_count": 2,
"units": 1,
"region": "eu",
"environment": "live",
"template": {
"id": "tpl_7c1e4b9a2d5f4c8e9b3a6d0f2e8c4a1b",
"slug": "invoice",
"version": 7,
"channel": "published",
"canary": false
},
"engine_version": "chromium 140.0.7339.16; engine jinja2",
"template_checksum": "9f2c…",
"output_sha256": "b41e…",
"deduplicated": false,
"storage": null,
"einvoice": null,
"accessibility": null,
"timings": {
"queue_ms": 0,
"compile_ms": 9,
"render_ms": 734,
"post_ms": 12,
"upload_ms": 41,
"total_ms": 812
},
"error": null,
"meta": { "order_id": "8812" },
"created_at": "2026-09-11T10:00:00.000Z",
"completed_at": "2026-09-11T10:00:01.000Z"
}
| Feld | Bedeutung |
|---|---|
id | Die ID des Renderings, rnd_ und 32 Hexadezimalzeichen |
status | queued, rendering, succeeded oder failed |
output | Das Dateiformat |
download_url, expires_at | Link zur Datei und wann der Link abläuft; null, bis das Rendering gelingt, und nachdem die Datei gelöscht wurde |
page_count, bytes | Seiten (PDF) und Dateigröße |
units | Berechnete Einheiten, in Schritten von 0,5; 0 bei Test-Schlüsseln und deduplizierten Renderings |
region, environment | Wo gerendert wurde (eu) und ob ein live- oder test-Schlüssel angefragt hat |
template | null bei html- und url-Quellen. id (tpl_…), slug, version (die gerenderte Version), channel (der Release Channel, aus dem sie kam, null bei einer Versionsnummer oder latest) und canary (ob der Canary-Anteil des Channels sie gewählt hat) |
engine_version, template_checksum, output_sha256 | Nachweis der Reproduzierbarkeit |
storage | Die Kopie im eigenen Bucket des Workspace, mit ihrem status, bucket und key; null, wenn es keine gibt |
einvoice | Der Bericht einer E-Rechnung: was erzeugt wurde, was der Validator gefunden hat und ob das PDF die Werte des XML zeigt; null, wenn das Rendering keine erzeugt hat |
accessibility | Das Urteil des Validators über die Datei als PDF/UA-1, mit den verletzten Regeln und ihren Elementen; null, wenn das Rendering settings.pdf.ua nicht gesetzt hat |
timings | Millisekunden in der Warteschlange, beim Füllen der Vorlage (compile_ms), in Chromium (render_ms), beim Post-Processing (post_ms), beim Hochladen und insgesamt; null, bis es fertig ist |
error | Wenn status den Wert failed hat: { code, message, … }, Code und Meldung des Problems, nicht das ganze Problem |
meta | Ihre Metadaten |
created_at, completed_at | Wann das Rendering angelegt und wann es fertig wurde |
Die Antwort-Header wiederholen das Wesentliche: X-Units-Charged, X-Units-Remaining (was der Organisation in der Periode bleibt, Prepaid-Einheiten eingeschlossen), X-Render-Region, X-Render-Engine (die Vorlagensprache, none bei einfachem HTML und URLs, dedup bei einer deduplizierten Antwort), X-Render-Deduplicated, Server-Timing mit den Zeiten und die RateLimit-*-Header, die unter Limits und Verbrauch beschrieben sind. Ist die Antwort die Datei eines Renderings, das settings.pdf.ua gesetzt hat, sagt X-Formfeed-PDF-UA conformant oder not-conformant.
Ein Rendering, das fehlschlägt, während die Anfrage wartet, antwortet mit seinem Problem und dem HTTP-Status seines Codes (etwa 422 template_runtime_error), mit der ID des fehlgeschlagenen Renderings in render_id. So finden Sie es im Render-Protokoll und unter GET /renders/{id}.
Reproduzierbarkeit
Jedes Rendering zeichnet den Chromium-Build und die verwendete Vorlagensprache auf (engine_version, etwa chromium 140.0.7339.16; engine jinja2), eine Prüfsumme der Vorlagenversion (template_checksum) und einen SHA-256 der Ausgabedatei (output_sha256). Dieselben Eingaben, gerendert mit demselben Chromium-Build, ergeben dasselbe Dokument. Die Datei selbst trägt den Zeitpunkt ihrer Erzeugung, zwei Renderings derselben Anfrage haben also verschiedene Werte von output_sha256. Speichern Sie beide Prüfsummen neben einem Vertrag oder einer Rechnung: template_checksum belegt später, welche Vorlagenversion das Dokument erzeugt hat, output_sha256, dass diese Datei nicht verändert wurde.
Das Render-Protokoll hält zu jedem Rendering einen ausführlicheren Nachweis: Neben diesen drei Angaben enthält er einen Hash der Daten und der Einstellungen, die Schriften, die Version des Brand Kits und die Versionen der geteilten Partials, die die Vorlage gelesen hat, sowie die Region, die gerendert hat. Die App zeigt ihn in den Details des Renderings unter Reproduzierbarkeit; die API liefert nur die drei Felder oben.
Eine vollständige Anfrage
curl https://api-eu.formfeed.dev/v1/renders \
-H "Authorization: Bearer $FORMFEED_API_KEY" \
-H "Idempotency-Key: order-8812-invoice" \
-H "Content-Type: application/json" \
-d '{
"template": "invoice",
"data": { "invoice": { "number": "2026-0042", "lines": [{ "description": "Beratung", "qty": 8, "price": 120 }] } },
"output": "pdf",
"filename": "rechnung-2026-0042.pdf",
"locale": "de-DE",
"settings": { "margin": { "top": "30mm" } },
"access": "signed",
"meta": { "order_id": "8812" }
}'