Brand Kit und Partials
Zwei Dinge gehören nicht zu einer Vorlage, sondern zur Organisation, und jede Vorlage übernimmt sie beim Rendern: das Brand Kit (Logo, Farben, Schriften, rechtlicher Fußtext) und die geteilten Partials (Briefköpfe, Fußzeilen und andere Fragmente, die per Name eingebunden werden). Diese Endpunkte nutzen formfeed brand pull und formfeed partials.
Beide gelten für die ganze Organisation: Ein API-Schlüssel aus jedem Workspace der Organisation liest und schreibt dasselbe Kit und dieselben Partials. Sie gehören zu jedem Plan.
| Endpunkt | Scope |
|---|---|
GET /brand | template:read |
GET /partials | template:read |
GET /partials/{name} | template:read |
PUT /partials/{name} | template:write |
DELETE /partials/{name} | template:write |
template:read gehört zu den Standard-Scopes eines neuen Schlüssels; template:write müssen Sie beim Anlegen des Schlüssels auswählen (Scopes). Ein Schlüssel ohne den Scope erhält 403 forbidden.
Brand Kit lesen
curl https://api.formfeed.dev/v1/brand \
-H "Authorization: Bearer $FORMFEED_API_KEY"
{
"version": 7,
"name": "Fennlor Studio GmbH",
"colors": { "primary": "#0f766e", "accent": "#f59e0b" },
"fonts": { "heading": "Inter", "body": "Source Serif 4" },
"font_size": "10pt",
"logo": {
"primary": "https://cdn.formfeed.dev/a/brand/…/primary-3f9a1c2b7d4e.svg",
"inverse": null,
"mark": null
},
"legal_footer": "Fennlor Studio GmbH · Musterstraße 1 · 12345 Musterstadt · HRB 00000",
"page_defaults": {
"paper": { "format": "A4", "landscape": false },
"margin": {
"top": "25mm",
"right": "18mm",
"bottom": "20mm",
"left": "18mm"
}
},
"updated_at": "2026-09-13T09:12:00Z"
}
Alles außer page_defaults und updated_at ist genau das Objekt brand, das Vorlagen sehen. Nicht gesetzte Werte sind null; colors ist immer ein Objekt, page_defaults {}, wenn nichts gesetzt ist.
| Feld | Typ | Bedeutung |
|---|---|---|
version | Ganzzahl | Steigt mit jeder Änderung des Kits. Renderings halten sie fest (siehe unten). |
name | String oder null | Der Firmenname, bis 120 Zeichen. |
colors | Objekt | Farb-Tokens (bis zu 24; Kleinbuchstaben, Ziffern und Bindestriche, beginnend mit einem Buchstaben) zu #rgb, #rrggbb oder #rrggbbaa. Jedes ist auch die CSS-Variable --brand-color-<token>. |
fonts.heading, fonts.body | String oder null | Namen von Schriftfamilien. |
font_size | String oder null | Die Grundgröße, in pt oder px (10pt). |
logo.primary, logo.inverse, logo.mark | URL oder null | Die drei Logo-Varianten auf dem CDN. |
legal_footer | String oder null | Bis 2.000 Zeichen. |
page_defaults | Objekt | paper (format: A3 bis A6, Letter, Legal oder Tabloid; landscape) und margin (top, right, bottom, left). Nur der Ausgangspunkt für eine neue Vorlage, die in der App angelegt wird; nie auf ein Rendering angewendet. |
updated_at | Zeitstempel oder null | Wann sich das Kit zuletzt geändert hat. |
Logos werden unter einem inhaltsadressierten Namen gespeichert, …/a/brand/<Organisations-ID>/<Variante>-<erste 12 Hex-Ziffern des SHA-256 der Datei>.<Endung>: Ein neues Logo ist eine neue URL, kein Browser-, CDN- oder Render-Cache kann also das alte weiter zeigen. Wie Dateien der Bibliothek werden sie ohne Signatur an jeden ausgeliefert, der die URL hat. Logos sind PNG, JPEG, WebP oder SVG bis 512 KB.
Das Brand Kit bearbeiten Inhaber und Admins in der App (Marke in der Navigation der Organisation); Editoren und Betrachter sehen es nur. Die API hat dafür keinen Schreib-Endpunkt, und ein Logo lässt sich nicht mit einem API-Schlüssel hochladen.
Geteilte Partials auflisten und lesen
curl https://api.formfeed.dev/v1/partials \
-H "Authorization: Bearer $FORMFEED_API_KEY"
{
"data": [
{
"name": "letterhead",
"engine": "jinja2",
"description": "Logo und Absenderzeile",
"version": 3,
"created_at": "2026-09-13T08:00:00Z",
"updated_at": "2026-09-13T09:30:00Z"
}
]
}
Die Liste enthält alle geteilten Partials der Organisation, nach Namen sortiert, in einer Antwort (eine Organisation hat höchstens 200, deshalb gibt es kein Blättern). Sie lässt source weg; GET /partials/letterhead liefert ein Partial mit Quelltext oder 404 not_found. Ein Partial hat keine ID: Sein Name ist seine Adresse, und der Name im Pfad wird kleingeschrieben gelesen.
| Feld | Typ | Bedeutung |
|---|---|---|
name | String | Kleinbuchstaben, Ziffern, - und _. |
engine | jinja2, liquid oder handlebars | Nur Vorlagen dieser Engine finden das Partial. |
description | String oder null | Bis 300 Zeichen. |
version | Ganzzahl | Steigt, wenn sich Quelltext oder Engine ändern; eine neue Beschreibung allein lässt sie gleich. Renderings halten sie fest. |
source | String | Der Quelltext. Nur in GET /partials/{name} und in der Antwort auf PUT. |
created_at, updated_at | Zeitstempel |
Partial anlegen oder aktualisieren
curl -X PUT https://api.formfeed.dev/v1/partials/letterhead \
-H "Authorization: Bearer $FORMFEED_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"engine": "jinja2",
"source": "<header><img src=\"{{ brand.logo.primary }}\" height=\"40\"> {{ brand.name }}</header>",
"description": "Logo und Absenderzeile",
"base_version": 3
}'
| Feld | Pflicht | Bedeutung |
|---|---|---|
engine | ja | jinja2, liquid oder handlebars. |
source | ja | Bis 256 KB (gemessen in UTF-8-Bytes). |
description | nein | Bis 300 Zeichen. Fehlt sie, bleibt die gespeicherte Beschreibung; null entfernt sie. |
base_version | nein | Die zuletzt gelesene version; siehe Konflikte. |
PUT legt das Partial an (201) oder ersetzt Engine und Quelltext, und die Beschreibung, wenn Sie eine senden (200). Es liefert das Partial mit source und seiner aktuellen version. Das nächste Rendering jeder Vorlage, die es einbindet, verwendet den neuen Quelltext, auch bei veröffentlichten Vorlagen; keine Version einer Vorlage ändert sich.
- Name: wird kleingeschrieben gespeichert (
Letterheadim Pfad wird zuletterhead); 1 bis 64 Kleinbuchstaben, Ziffern, Bindestriche und Unterstriche, beginnend und endend mit einem Buchstaben oder einer Ziffer, eindeutig in der Organisation. Alles andere ist400 invalid_request. - Engine:
jinja2,liquidoderhandlebars. Nur Vorlagen dieser Engine finden das Partial. - Quelltext: bis zu 256 KB. Ein größerer oder ein ungültiger Body ergibt
400 invalid_request.
Konflikte mit base_version
Senden Sie die zuletzt gelesene version als base_version. Hat jemand das Partial zwischendurch geändert, antwortet die API mit 409 conflict und schreibt nichts; das Problem enthält das gespeicherte Partial als current (null, wenn es kein Partial dieses Namens gibt, weil es gelöscht oder nie angelegt wurde):
{
"type": "https://docs.formfeed.dev/errors/conflict",
"status": 409,
"code": "conflict",
"detail": "The partial is at version 4, not 3; pull it first",
"current": {
"name": "letterhead",
"engine": "jinja2",
"version": 4,
"…": "…"
}
}
Lesen Sie das Partial erneut, führen Sie Ihre Änderung zusammen und versuchen Sie es mit der neuen Version noch einmal. Ohne base_version wird bedingungslos überschrieben, und nur ohne base_version lässt sich ein neues Partial anlegen. Das ist dasselbe Muster wie base_checksum bei Versionen von Vorlagen.
Das Limit von 200 Partials
Eine Organisation hat höchstens 200 geteilte Partials, in jedem Plan. Ein weiteres anzulegen beantwortet die API mit 400 invalid_request und "feature": "partials"; löschen Sie zuerst Partials, die Sie nicht mehr brauchen. Ein bestehendes Partial zu aktualisieren ist immer möglich.
Partial löschen
curl -X DELETE https://api.formfeed.dev/v1/partials/letterhead \
-H "Authorization: Bearer $FORMFEED_API_KEY"
204 bei Erfolg, 404 not_found, wenn es kein Partial dieses Namens gibt. Vorlagen, die es noch einbinden, schlagen ab dem nächsten Rendering mit einem Vorlagenfehler fehl, bis das Include entfernt ist oder wieder ein Partial dieses Namens existiert. Die Seite Partials in der App zeigt vor dem Löschen, welche Vorlagen ein Partial verwenden.
Wie Renderings sie verwenden
- Welche Renderings. Ein Rendering einer gespeicherten HTML-Vorlage (PDF oder Bild) oder ein Ad-hoc-
html-Rendering mitenginebekommt das aktuelle Brand Kit und die geteilten Partials, die es einbindet. Eine Word- oder PowerPoint-Vorlage bekommt nur das Brand Kit, weil ihr Dokument keine Partials einbinden kann. Reines HTML ohneengineundurl-Renderings bekommen keines von beiden. - Welche Partials. Nur die, die Inhalt, Kopf- oder Fußzeile einbinden, direkt oder über andere geteilte Partials, reisen mit dem Rendering. Ein Include trifft ein geteiltes Partial der Engine der Vorlage ohne Rücksicht auf Groß- und Kleinschreibung und mit einer ignorierten Dateiendung (
.html,.htm,.j2,.njk,.jinja,.liquid,.hbs,.handlebars);{% include "Letterhead.html" %}findet alsoletterhead. Ein Partial, das mit der Version der Vorlage unter demselben Namen gespeichert ist, hat Vorrang vor dem geteilten (wie ein Name aufgelöst wird). Die geteilten Partials eines Renderings dürfen zusammen 2 MB groß sein; mehr ergibt400 invalid_request. - Wann Änderungen ankommen. Das Gateway hält Kit und Partials bis zu 60 Sekunden im Cache. Ein Schreibzugriff über diese Endpunkte leert diesen Cache dort, wo er lief, und gilt deshalb meist sofort; eine Änderung in der App erreicht API-Renderings innerhalb einer Minute.
- Wenn sie sich nicht lesen lassen. Ein Rendering läuft nicht ohne sie, denn ohne Kit fehlte der rechtliche Fußtext. Lassen sie sich nicht laden, scheitert die Anfrage mit
500 worker_exhausted, bevor ein Rendering erfasst wird, und kostet nichts; wiederholen Sie sie mit demselben Idempotency-Key. - Was festgehalten wird. Jedes Rendering hält die Version des Brand Kits (
brand_version) und die Versionen der verwendeten geteilten Partials (shared_partials) in seinem Reproduzierbarkeits-Datensatz fest, den die Details eines Renderings auf der Seite Renderings der App zeigen. Die Deduplizierung verwendet ein Ergebnis nur wieder, wenn beides unverändert ist.
POST /templates/{id}/validate prüft eine Version mit demselben Brand Kit und denselben geteilten Partials, die ein Rendering bekäme.
Mit den SDKs
Die SDKs für TypeScript und Python bieten diese Endpunkte als brand.get() sowie partials.list(), get, put und delete. put liefert das Partial und ob es neu angelegt wurde, und ein Konflikt löst den Fehler des SDKs aus, mit dem gespeicherten Partial in seinem Problem:
import { Formfeed, FormfeedError } from '@formfeed/sdk';
const formfeed = new Formfeed({ apiKey: process.env.FORMFEED_API_KEY! });
const current = await formfeed.partials.get('letterhead');
try {
const { partial, created } = await formfeed.partials.put('letterhead', {
engine: current.engine,
source: current.source!.replace('height="40"', 'height="48"'),
base_version: current.version,
});
console.log(created ? 'angelegt' : `jetzt Version ${partial.version}`);
} catch (e) {
if (e instanceof FormfeedError && e.code === 'conflict')
console.log(e.problem?.['current']);
else throw e;
}
import os
from formfeed import Formfeed, FormfeedError
client = Formfeed(os.environ["FORMFEED_API_KEY"])
current = client.partials.get("letterhead")
try:
result = client.partials.put(
"letterhead",
engine=current.engine,
source=current.source.replace('height="40"', 'height="48"'),
base_version=current.version,
)
print("angelegt" if result.created else f"jetzt Version {result.partial.version}")
except FormfeedError as e:
if e.code != "conflict":
raise
print(e.problem["current"])