Zum Inhalt springen

Anwendungsfall

Verträge und Vereinbarungen als PDF

Formfeed ist eine API, die aus einer Vertragsvorlage und den Bedingungen eines Geschäfts das fertige PDF macht: Dienstleistungsverträge, NDAs, Angebote, Bestellformulare. Eine Klausel erscheint nur dort, wo die Daten sie verlangen, und die Nummerierung folgt von selbst. Der Wortlaut hat Versionen; jedes Rendering sagt also, aus welchem Text es gedruckt wurde, und dieselbe Anfrage kann Ihre AGB anhängen und die Datei mit einem Passwort verschließen.

Kostenloser Plan mit 100 Einheiten im Monat, keine Kreditkarte

Klauseln je nach Geschäft

Ein if in der Vorlage zeigt eine Klausel für einen Kunden und lässt sie für den nächsten weg, eine Schleife druckt einen Unterschriftsblock je Unterzeichner, und ein Partial hält den Absatz, den mehrere Verträge teilen.

Nummerierung, die folgt

Klauselnummern sind CSS-Zähler, kein Text. Entfällt eine Klausel, nummeriert sich alles danach neu, und numToWords schreibt eine Laufzeit, wie Verträge es tun: „zwei (2) Jahre“.

Wortlaut mit Versionen

Jedes Speichern ist eine neue Version, mit einer Notiz zur Änderung, wenn Sie eine schreiben, und die API rendert die veröffentlichte Version, solange eine Anfrage keine andere nennt. Ein Rendering speichert die verwendete Version und eine Prüfsumme davon; Sie können also später sagen, aus welchem Wortlaut ein Vertrag gedruckt wurde.

Bearbeitet von denen, denen der Text gehört

Mit „Text bearbeiten“ wird ein Absatz direkt in der Vorschau der Seite geändert, ohne Code zu lesen. Werte aus den Daten bleiben, wie sie sind, und die gespeicherte Änderung ist eine Version wie jede andere.

AGB angehängt, Datei verschlossen

Eine Anfrage hängt Ihre AGB aus der Dateibibliothek an, stempelt ENTWURF über jede Seite und verschlüsselt das PDF mit AES-256. Ab dem Starter-Plan.

Ihre Word-Datei als Vorlage

Ein Vertrag, den es als Word-Datei gibt, kann eine bleiben: Tippen Sie die Tags ins Dokument, laden Sie es hoch, und ein Rendering gibt es gefüllt zurück, als DOCX zum Verhandeln oder als PDF zum Unterschreiben. Ab dem Starter-Plan.

Ein Vertrags-PDF aus JSON erzeugen

Ein Vertrag ist die Vorlage plus die Bedingungen eines Geschäfts. Speichern Sie die Daten als data.json und senden Sie sie mit dem Namen der Vorlage; die Antwort enthält den Link zum PDF. Das Beispiel ist ein gegenseitiges NDA in Handlebars: Parteien, Zweck, Laufzeit und Unterzeichner kommen aus den Daten, der Wortlaut aus der Vorlage. 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.

data.jsonJSON
{
  "agreement": {
    "date": "2026-09-28",
    "effective": "2026-10-01",
    "purpose": "a joint catalogue of lighting products for the 2027 season",
    "includes": [
      "product designs and drawings",
      "prices and margins",
      "customer and supplier lists",
      "business plans"
    ],
    "term_years": 2,
    "survival_years": 3,
    "non_solicitation_months": 12,
    "governing_law": "England and Wales"
  },
  "party_a": {
    "name": "Fennlor Studio Ltd",
    "jurisdiction": "England and Wales",
    "number": "00000000",
    "address": "1 Example Street, Exampletown"
  },
  "party_b": {
    "name": "Olvarest Ltd",
    "jurisdiction": "England and Wales",
    "number": "00000000",
    "address": "2 Example Road, Exampleville"
  },
  "signatories": [
    {
      "party": "Fennlor Studio Ltd",
      "name": "Jane Doe",
      "title": "Director"
    },
    {
      "party": "Olvarest Ltd",
      "name": "John Doe",
      "title": "Managing Director"
    }
  ]
}
render.shShell
jq '{template: "mutual-nda", data: .}' data.json \
  | curl https://api-eu.formfeed.dev/v1/renders \
      -H "Authorization: Bearer ff_test_..." \
      -H "Content-Type: application/json" \
      --data-binary @-
render.tsTypeScript
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: 'mutual-nda',
  data,
});
console.log(render.status, render.download_url);
render.pyPython
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="mutual-nda",
    data=data,
)
print(render.status, render.download_url)
render.phpPHP
<?php
$body = [
    'template' => 'mutual-nda',
    '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;
main.goGo
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": "mutual-nda",
		"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)
}
Program.csC#
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

var body = new
{
    template = "mutual-nda",
    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")}");
Render.javaJava
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": "mutual-nda",
          "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 …
  }
}
render.rbRuby
require "json"
require "net/http"

body = {
  template: "mutual-nda",
  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.

PDF · 2 Seiten · 147 KB ↓ herunterladen

Diese Vorlage verwenden

Kostenloser Plan, keine Kreditkarte. Sie erhalten eine eigene Kopie im Editor, veröffentlicht und bereit für die API.

Klauseln, die nur erscheinen, wenn sie gelten

Der größte Teil eines Vertrags ist für alle gleich. Was sich unterscheidet, sind eine Handvoll Werte und ein paar Klauseln. Die Werte sind Felder der Daten. Eine Klausel, die nur manche Geschäfte haben, steht in einer Bedingung: Das Beispiel druckt seine Abwerbeklausel, wenn die Daten ihr eine Zahl von Monaten geben, {{#if agreement.non_solicitation_months}}, und lässt sie sonst weg.

Sicher wird das durch die Nummerierung. Die Nummern schreiben CSS-Zähler; die Klausel nach einer ausgelassenen rückt also von selbst auf, und kein Vertrag geht hinaus, in dem auf Klausel 5 die Klausel 7 folgt.

style.cssCSS
ol.clauses { margin: 6mm 0 0; padding: 0; list-style: none; counter-reset: clause; }
ol.clauses > li { counter-increment: clause; margin-bottom: 4mm; }
ol.clauses h2::before { content: counter(clause) ". "; }

Daten und Zahlen kommen von Helfern: date formatiert ein Datum für die Sprache der Vorlage, dateAdd rechnet den Tag aus, an dem eine Laufzeit endet, und numToWords schreibt eine Zahl auf Deutsch, Englisch, Französisch, Spanisch, Italienisch oder Niederländisch aus. Der Unterschriftsblock hat break-inside: avoid; eine Unterschriftslinie landet also nie ohne ihren Namen auf einer Seite. Das NDA-Beispiel ist die vollständige Vorlage mit ihren Daten.

Kein Vertrag mit einer Lücke

Ein Feld, das Ihr Code vergessen hat, wird als leerer Text gedruckt, und ein Vertrag mit leerem Parteinamen ist schlechter als kein Vertrag. Speichern Sie ein Datenschema bei der Vorlage, kennzeichnen Sie die Felder, ohne die ein Vertrag nicht auskommt, als Pflichtfelder, und senden Sie "validate_data": true mit dem Rendering. Daten, die nicht passen, werden dann mit 422 data_validation_error und einer Liste aller fehlenden oder falschen Felder abgelehnt, bevor etwas gerendert oder gezählt wird.

Dasselbe Schema wirkt, bevor überhaupt eine Anfrage gesendet wird. Der Editor unterstreicht Variablen, die in den Beispieldaten fehlen, formfeed types macht aus dem Schema TypeScript- oder Python-Typen, sodass ein fehlendes Feld schon in Ihrem eigenen Editor ein Fehler ist, und das Veröffentlichen einer Version, deren Schema Daten ablehnen würde, die die veröffentlichte annimmt, wird angehalten, bis jemand es erlaubt. Mehr unter typisierte Vorlagendaten.

Wortlaut mit Geschichte

Eine Vertragsvorlage ändert sich, wenn die Rechtsabteilung einen Satz ändert, und es kommt darauf an, welche Verträge vorher hinausgegangen sind. Jedes Speichern einer Vorlage ist eine neue Version, nie die Änderung einer alten. Die API rendert die veröffentlichte Version; ein neuer Wortlaut gilt also, wenn ihn jemand veröffentlicht, nicht wenn er gespeichert wird. Im Editor steht jede gespeicherte Version auf Wunsch neben der, an der Sie arbeiten, und eine ältere lässt sich als neuer Entwurf wiederherstellen.

Wem der Wortlaut gehört, der muss kein HTML lesen. Mit Text bearbeiten klicken Inhaber, Admins und Editoren in der Vorschau auf einen Absatz und tippen, und die Werte aus den Daten bleiben Werte; Text mit einer Bedingung darin wird im Code geändert. Ein Absatz, den mehrere Verträge teilen, eine Haftungsklausel oder ein Unterschriftsblock, kann ein geteiltes Partial sein: Einmal geändert, steht der neue Text binnen einer Minute in jeder Vorlage, die ihn einbindet, auch in den veröffentlichten. Ab dem Starter-Plan hat eine Vorlage außerdem Release Channels; ein Staging-System rendert dann den Wortlaut, der für es freigegeben wurde.

Welchen Text haben sie erhalten?

Jedes Rendering speichert, woraus es entstanden ist: die Nummer der Vorlagenversion, eine Prüfsumme dieser Version (template_checksum), den Chromium-Build und die Vorlagensprache (engine_version) und einen SHA-256 der Datei (output_sha256). Bewahren Sie die beiden Prüfsummen beim Vertrag in Ihrem eigenen System auf: Die erste sagt später, welche Version des Wortlauts die Datei erzeugt hat, die zweite, dass eine Datei Byte für Byte die gerenderte ist. meta in der Anfrage trägt Ihre Vorgangs- oder Kundennummer in das Rendering, das Render-Protokoll und jeden Webhook.

Ein geteiltes Partial ist von dieser Prüfsumme nicht erfasst, weil es sich ohne neue Version der Vorlage ändert. Das Rendering hält die Version jedes geteilten Partials fest, das es verwendet hat, und sein Eintrag in der App zeigt sie. Wortlaut, der an eine Version eines Vertrags gebunden sein muss, gehört in die Vorlage selbst.

AGB anhängen, Entwurf kennzeichnen, Datei verschließen

Ein Vertrag reist selten allein. post führt PDF-Schritte am fertigen Dokument im selben Rendering aus: merge_after hängt weitere PDFs an, etwa Ihre AGB aus der Dateibibliothek oder eine eben gerenderte Anlage, watermark stempelt einen Text oder ein Bild über jede Seite, und password verschlüsselt das Ergebnis mit AES-256.

POST /v1/rendersJSON
{
  "template": "mutual-nda",
  "data": { … },
  "post": {
    "merge_after": ["terms.pdf"],
    "watermark": { "text": "DRAFT", "opacity": 0.15 },
    "password": { "user": "open-sesame", "permissions": ["print"] }
  }
}

Die Schritte laufen immer in dieser Reihenfolge, gleich in welcher die Anfrage sie nennt, weil ein verschlüsseltes PDF nicht mehr geändert werden kann. Jeder Schritt kostet eine halbe Einheit zusätzlich zum Rendering, und Post-Processing setzt den Starter-Plan voraus. Ein Passwort zum Öffnen hält die Datei vertraulich; die Berechtigungen (print, copy, modify, annotate) beachten PDF-Viewer, sie sind aber kein Schutz gegen jemanden, der die Datei öffnen kann. Dateien der Bibliothek sind für jeden öffentlich, der ihre Adresse kennt, und laufen nicht ab: richtig für AGB, falsch für alles Vertrauliche.

Dieselben drei Werkzeuge arbeiten auch auf Dokumenten, die Sie früher gerendert haben, über POST /v1/pdf/merge, /pdf/watermark und /pdf/protect: siehe PDF-Tools. Das Angebot als Entwurf und die vertrauliche Gehaltsabrechnung sind Beispiele mit Wasserzeichen und Passwort in ihrer Anfrage.

Wenn der Vertrag eine Word-Datei ist

Viele Verträge gibt es als Word-Dokument, das die Rechtsabteilung freigegeben hat, und ein Nachbau in HTML hieße, es noch einmal freizugeben. Ab dem Starter-Plan kann diese Datei die Vorlage sein: Tippen Sie dieselben Tags in ihren Text, {{ party_a.name }}, wo ein Name hingehört, eine Bedingung allein in einem Absatz vor und nach einer Klausel, und laden Sie die Datei hoch. Ein Rendering füllt eine Kopie und gibt sie als DOCX zurück, für eine Gegenseite, die darin kommentieren will, oder als PDF. Papier, Ränder, Kopf- und Fußzeilen bleiben, was das Dokument sagt; eine Änderung am Wortlaut wird in Word gemacht und neu hochgeladen. Mehr unter Word- und PowerPoint-Vorlagen.

Was ein Vertrag kostet

Ein Vertrag mit bis zu fünf Seiten ist eine Einheit, und jede weiteren fünf Seiten kosten eine mehr. AGB anhängen, ein Wasserzeichen und ein Passwort kosten je eine halbe Einheit. Der Free-Plan hat 100 Einheiten im Monat und verlangt keine Kreditkarte; Post-Processing und Word-Vorlagen beginnen mit Starter, das 3.000 Einheiten für 19 € im Monat hat. 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 den Vertrag, den Ihre Vorlage und Ihre Daten beschreiben. Es schreibt den Wortlaut nicht und prüft ihn nicht: Das Beispiel zeigt die Mechanik, und Ihren Text sollte eine Juristin oder ein Jurist lesen, bevor er hinausgeht. Es holt keine Unterschriften ein und fügt keine Signaturfelder ein; das PDF geht an den Signaturdienst, den Sie nutzen, oder in den Drucker. Und es ist nicht Ihr Archiv: 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 jeden Vertrag selbst. Ab dem Pro-Plan geht von jeder Datei beim Rendern eine Kopie in Ihren eigenen S3- oder R2-Bucket.

FAQ

Übernimmt Formfeed elektronische Signaturen?
Nein. Formfeed erzeugt das Dokument; übergeben Sie es dem Signaturdienst, den Sie nutzen. Mit output_sha256 prüfen Sie, dass die Datei, die Sie weitergeben, die gerenderte ist. Ein signiertes PDF ist eine geänderte Datei und hat einen eigenen Hash.
Können Nicht-Entwickler den Wortlaut ändern?
Ja. Mit „Text bearbeiten“ ändern Inhaber, Admins und Editoren einen Absatz direkt in der Vorschau; Werte aus den Daten bleiben, wie sie sind, und jede gespeicherte Änderung ist eine Version, die ein Entwickler prüfen kann. Text mit einer Bedingung darin wird im Code geändert, und Entwickler verantworten den Datenvertrag.
Unsere Vertragsvorlage ist eine Word-Datei. Müssen wir sie nachbauen?
Nein. Tippen Sie die Tags in die Word-Datei und laden Sie sie als Word-Vorlage hoch; Formfeed befüllt sie und liefert DOCX oder PDF. Word- und PowerPoint-Vorlagen gibt es ab Starter.
Lässt sich das PDF mit einem Passwort schützen?
Ja, ab dem Starter-Plan: post.password verschlüsselt die Datei mit AES-256, mit einem Passwort zum Öffnen und Berechtigungen für Drucken, Kopieren, Ändern und Kommentieren. Das kostet eine halbe Einheit.
Wie belegen wir später, welche Version der Bedingungen ein Vertrag verwendet hat?
Jedes Rendering speichert die Version der Vorlage und eine Prüfsumme davon, und die Datei hat einen SHA-256. Bewahren Sie beide beim Vertrag auf; das Render-Protokoll behält seinen Eintrag 13 Monate lang, länger nur, solange die Datei selbst gespeichert ist.
Ist der Wortlaut des Beispiels einsatzbereit?
Nein. Das NDA-Beispiel zeigt, wie eine Vertragsvorlage funktioniert: Bedingungen, Nummerierung, Daten und Unterschriften. Lassen Sie jeden Wortlaut juristisch prüfen, bevor Sie ihn verwenden.

Weitere Anwendungsfälle

Heute mit dem Rendern beginnen

Der kostenlose Plan enthält 100 Einheiten im Monat und kostenlose Testrenderings. Wechseln Sie den Plan, wenn Sie mehr brauchen.