Zum Hauptinhalt springen

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.

EndpunktScope
GET /brandtemplate:read
GET /partialstemplate: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.

FeldTypBedeutung
versionGanzzahlSteigt mit jeder Änderung des Kits. Renderings halten sie fest (siehe unten).
nameString oder nullDer Firmenname, bis 120 Zeichen.
colorsObjektFarb-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.bodyString oder nullNamen von Schriftfamilien.
font_sizeString oder nullDie Grundgröße, in pt oder px (10pt).
logo.primary, logo.inverse, logo.markURL oder nullDie drei Logo-Varianten auf dem CDN.
legal_footerString oder nullBis 2.000 Zeichen.
page_defaultsObjektpaper (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_atZeitstempel oder nullWann 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.

FeldTypBedeutung
nameStringKleinbuchstaben, Ziffern, - und _.
enginejinja2, liquid oder handlebarsNur Vorlagen dieser Engine finden das Partial.
descriptionString oder nullBis 300 Zeichen.
versionGanzzahlSteigt, wenn sich Quelltext oder Engine ändern; eine neue Beschreibung allein lässt sie gleich. Renderings halten sie fest.
sourceStringDer Quelltext. Nur in GET /partials/{name} und in der Antwort auf PUT.
created_at, updated_atZeitstempel

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
}'
FeldPflichtBedeutung
enginejajinja2, liquid oder handlebars.
sourcejaBis 256 KB (gemessen in UTF-8-Bytes).
descriptionneinBis 300 Zeichen. Fehlt sie, bleibt die gespeicherte Beschreibung; null entfernt sie.
base_versionneinDie 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 (Letterhead im Pfad wird zu letterhead); 1 bis 64 Kleinbuchstaben, Ziffern, Bindestriche und Unterstriche, beginnend und endend mit einem Buchstaben oder einer Ziffer, eindeutig in der Organisation. Alles andere ist 400 invalid_request.
  • Engine: jinja2, liquid oder handlebars. 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 mit engine bekommt 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 ohne engine und url-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 also letterhead. 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 ergibt 400 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"])