Dateien
Jeder Workspace hat eine Dateibibliothek: die Bilder, die eine Vorlage zeigt, und die PDFs, die sie zusammenführt. Eine Datei wird über ihren Namen adressiert, nicht über eine ID. Eine Vorlage schreibt also
- Jinja2
- Liquid
- Handlebars
<img src="{{ asset('logo.png') }}" alt="" />
<img src="{{ 'logo.png' | asset }}" alt="" />
<img src="{{asset 'logo.png'}}" alt="" />
und funktioniert weiter, wenn Sie ein neues logo.png hochladen. asset() liefert die öffentliche
CDN-URL der Datei. Auch relative Pfade werden gegen die Bibliothek aufgelöst: <img src="logo.png">
in einer Vorlage oder in html, das Sie als Fragment senden, findet dieselbe Datei. (Ein vollständiges
HTML-Dokument, das mit <!doctype oder <html> beginnt, bekommt diese Basis nicht; verwenden Sie dort
die vollständige URL.)
Die Bibliothek nimmt nur Bilder und PDFs auf. Word- und PowerPoint-Dateien gehören woandershin: Eine Word- oder PowerPoint-Vorlage bewahrt ihr Dokument mit der Version der Vorlage auf, und ein Dokument, das Sie in PDF umwandeln, wird gar nicht gespeichert.
Hochladen
curl -X POST https://api.formfeed.dev/v1/files \
-H "Authorization: Bearer $FORMFEED_API_KEY" \
-F file=@logo.png
{
"id": "fil_5b0000000000400080000000000000a1",
"name": "logo.png",
"content_type": "image/png",
"bytes": 8421,
"sha256": "3f786850e387550fdab836ed7e6dc881de23001b…",
"url": "https://cdn.formfeed.dev/a/f0000000-0000-4000-8000-000000000001/logo.png",
"created_at": "2026-09-12T10:00:00Z",
"updated_at": "2026-09-12T10:00:00Z"
}
Die Anfrage ist multipart/form-data mit zwei Teilen:
| Teil | Pflicht | Bedeutung |
|---|---|---|
file | ja | Die Datei. Ihr Content-Type kommt aus dem Typ des Teils, wenn die Bibliothek ihn annimmt, sonst aus der Dateiendung des Namens. |
name | nein | Der Name in der Bibliothek. Ohne ihn gilt der Name der hochgeladenen Datei, bereinigt: Jedes Zeichen, das ein Name nicht enthalten darf, wird zu -. Ein name, den Sie senden, muss bereits gültig sein (sonst 400 invalid_request). |
Ein Name hat bis zu 200 Zeichen aus Buchstaben, Ziffern, Punkt, Bindestrich und Unterstrich, mit /
für Ordner (brand/header.svg); jeder Teil zwischen den Schrägstrichen beginnt mit einem Buchstaben
oder einer Ziffer. Groß- und Kleinschreibung zählt: Logo.png und logo.png sind zwei Dateien.
Die Antwort beschreibt die Datei: id (fil_…, zum Lesen und Löschen), name, content_type,
bytes, den sha256 des Inhalts, die öffentliche url sowie wann sie angelegt und zuletzt ersetzt
wurde.
Derselbe Name ersetzt die vorhandene Datei und antwortet mit 200 statt 201. ID, URL und jeder
Verweis in jeder Vorlage bleiben gleich; Vorlagen übernehmen ein neues Logo also ohne neue Version.
Die neue Datei erreicht aber nicht sofort jedes Dokument; siehe Caching.
| Typen | PNG, JPEG, WebP, GIF, SVG, AVIF, PDF |
| Größe | 10 MB pro Datei (darüber 413 file_too_large) |
| Anzahl | 1.000 Dateien pro Workspace, in jedem Plan; eine weitere ergibt 400 invalid_request mit "feature": "files". Ersetzen ist immer möglich. |
| Pläne | Alle Pläne |
| Scopes | file:read zum Auflisten und Lesen, file:write zum Hochladen (schließt file:read ein), file:delete zum Löschen. Keiner davon ist ein Standard-Scope: Wählen Sie sie beim Anlegen des Schlüssels (Scopes). |
| In der App | Die Seite Dateien. Editoren laden hoch und ersetzen, Inhaber und Admins löschen, Betrachter sehen die Liste. |
| Einheiten | Kostenlos. Uploads werden nicht gezählt. |
Eine Datei, deren Bytes nicht zum Content-Type passen, wird mit 400 invalid_request abgelehnt: Die
Bibliothek wird über das CDN ausgeliefert, eine „HTML-Seite namens logo.png“ darf dort nicht hinein.
Eine leere Datei und ein Typ außerhalb der Liste werden genauso abgelehnt.
Schriften der Organisation gehören nicht in diese Bibliothek. Sie haben ihre eigene Seite in der App, weil eine Schrift an der Organisation hängt und in jedes Rendering eingebaut wird (siehe Vorlagen schreiben).
Mit den SDKs
import { readFile } from 'node:fs/promises';
import { Formfeed } from '@formfeed/sdk';
const formfeed = new Formfeed({ apiKey: process.env.FORMFEED_API_KEY! });
const logo = await formfeed.files.upload({
data: await readFile('logo.png'),
name: 'brand/logo.png',
});
console.log(logo.url);
import os
from formfeed import Formfeed
client = Formfeed(os.environ["FORMFEED_API_KEY"])
with open("logo.png", "rb") as f:
logo = client.files.upload(f.read(), "brand/logo.png")
print(logo.url)
Beide SDKs senden den übergebenen Namen als name; er muss also ein gültiger Name der Bibliothek sein.
Außerdem gibt es files.list(), files.all() (alle Seiten), files.get(id) und files.delete(file).
Im Post-Processing
Ein PDF aus der Bibliothek ist eine Merge-Quelle, ein PNG oder JPEG ein Wasserzeichen:
{
"template": "invoice",
"data": { "...": "..." },
"post": {
"merge_after": ["terms.pdf"],
"watermark": { "image": "draft-stamp.png", "opacity": 0.15 }
}
}
Benennen Sie Dateien der Bibliothek hier über ihren Namen oder ihre Datei-ID (fil_…);
dieselben Felder nehmen auch Rendering-IDs (rnd_…) früherer Renderings. Merges nehmen nur PDFs,
Wasserzeichen nur PNG und JPEG; ein anderer Typ ergibt
422 source_invalid, eine Datei, die nicht in der Bibliothek liegt,
422 source_not_found. Siehe PDF-Tools.
Auflisten, lesen und löschen
curl "https://api.formfeed.dev/v1/files?prefix=brand/&limit=50" \
-H "Authorization: Bearer $FORMFEED_API_KEY"
curl https://api.formfeed.dev/v1/files/fil_5b0000000000400080000000000000a1 \
-H "Authorization: Bearer $FORMFEED_API_KEY"
curl -X DELETE https://api.formfeed.dev/v1/files/fil_5b0000000000400080000000000000a1 \
-H "Authorization: Bearer $FORMFEED_API_KEY"
| Query-Parameter | Standard | Bedeutung |
|---|---|---|
prefix | keiner | Nur Namen, die damit beginnen, etwa ein Ordner: brand/. |
limit | 50 | Seitengröße, 1 bis 100 (außerhalb davon 400 invalid_request). |
cursor | keiner | Der next_cursor der vorherigen Seite. |
Die Liste antwortet mit { "data": [ … ], "next_cursor": … } und beginnt mit der neuesten Datei;
auf der letzten Seite ist next_cursor null. GET /files/{id} liest eine
Datei, DELETE /files/{id} löscht sie, beide über ihre fil_…-ID, nicht über den Namen; eine
unbekannte ID ergibt 404 not_found. Löschen antwortet mit 204 und entfernt
die Bytes sofort aus dem Speicher. Vorlagen, die den Namen verwenden, zeigen danach ein fehlendes Bild,
sobald die Caches unten die Datei losgelassen haben.
Caching
Eine Datei wird an zwei Stellen zwischengespeichert, beide Male unter ihrer URL, die sich beim Ersetzen nicht ändert:
- Das CDN antwortet mit
Cache-Control: public, max-age=3600. Browser und Edge halten eine Datei bis zu eine Stunde: Eine Ersetzung erreicht alle Betrachter, die Vorschau im Editor eingeschlossen, innerhalb dieser Stunde, und eine gelöschte Datei kann bis zum Ablauf dort noch ausgeliefert werden. - Der Render-Worker hält die Bilder, die ein Dokument lädt (über
asset(), einen relativen Pfad oder ein<img>mit der URL der Bibliothek), bis zu eine Woche in seinem eigenen Cache. Ein ersetztes oder gelöschtes Bild kann also bis zu eine Woche lang noch in gerenderten Dokumenten erscheinen.
Wasserzeichenbilder und Merge-Quellen werden direkt aus dem Speicher gelesen und sind immer aktuell.
Muss eine Änderung schon im nächsten Dokument sichtbar sein, laden Sie die Datei unter einem neuen
Namen hoch (logo-2026.png) und stellen die Vorlage darauf um.
Was die Bibliothek nicht ist
Dateien der Bibliothek werden ohne Signatur ausgeliefert: Wer die URL hat, kann sie abrufen, und die URL besteht aus der ID Ihres Workspaces und dem Namen der Datei. Legen Sie Kundendokumente nicht dort ab — dafür gibt es die gerenderten Ausgaben mit signierten, ablaufenden Links.
Das CDN liefert Dateien der Bibliothek mit Content-Security-Policy: sandbox aus, sodass eine direkt
geöffnete SVG- oder andere Datei kein Skript ausführt, und mit Access-Control-Allow-Origin: *, sodass
jede Seite sie laden darf.