Für Entwickler
PDF-Formulare per API ausfüllen
Was die API kann
Felder erkennen
POST /detect_form liefert ein JSON-Schema mit Position, Art und Namen jedes Feldes – auch für Scans und PDFs ohne Formularfelder.
Formulare ausfüllen
POST /edit füllt aus Anweisungen in natürlicher Sprache, aus beliebigem JSON (config.data) oder aus Dokumenten und liefert ein PDF plus filledValues.
Eigenes Schema
Mit einem gespeicherten Schema überspringen Sie die Erkennung; inputSchema ordnet Ihr eigenes Datenmodell den Feldern zu.
Asynchron und Webhooks
Lange Formulare über /edit_runs starten und per Webhook benachrichtigen lassen – signiert, mit Wiederholungen.
Sauberes Schreiben
Kästchen für einzelne Ziffern, Datumsformate, Ankreuzfelder, Tabellen, Unterschriften; standardmäßig fest eingebettet, auf Wunsch als bearbeitbare Felder. Digital signierte PDFs werden inkrementell gespeichert, damit die Signaturen erhalten bleiben.
Prüfliste
Unsichere oder gekürzte Werte stehen in output.review – ideal für eine menschliche Kontrolle vor dem Versand.
Schnellstart
FORMFILL_API_URL und einen Schlüssel aus dem Dashboard; Testschlüssel beginnen mit ff_test_.export FORMFILL_API_URL="https://api.ausfuellpilot.de"
export FORMFILL_API_KEY="ff_test_…"
# 1. PDF hochladen
FILE_ID=$(curl -sS "$FORMFILL_API_URL/files/upload" \
-H "Authorization: Bearer $FORMFILL_API_KEY" \
-F file=@formular.pdf | jq -r .id)
# 2. Ausfüllen (synchron; für lange Formulare: POST /edit_runs und GET /edit_runs/:id)
curl -sS "$FORMFILL_API_URL/edit" \
-H "Authorization: Bearer $FORMFILL_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"file": { "id": "'"$FILE_ID"'" },
"config": {
"instructions": "Fülle das Formular für Erika Mustermann aus, geboren am 12.08.1990 in Musterstadt, wohnhaft Musterstraße 1, 12345 Musterstadt. Unterschreibe mit ihrem Namen und dem heutigen Datum.",
"advancedOptions": { "flattenPdf": true }
}
}' > run.json
# 3. Ergebnis herunterladen (Link 15 Minuten gültig)
curl -sS -o ausgefuellt.pdf "$(jq -r .output.editedFile.presignedUrl run.json)"import { readFile, writeFile } from "node:fs/promises";
const API = process.env.FORMFILL_API_URL!;
const auth = { Authorization: `Bearer ${process.env.FORMFILL_API_KEY}` };
// 1. PDF hochladen
const form = new FormData();
form.append("file", new Blob([await readFile("formular.pdf")], { type: "application/pdf" }), "formular.pdf");
const file = await (await fetch(`${API}/files/upload`, { method: "POST", headers: auth, body: form })).json();
// 2. Ausfüllen
const res = await fetch(`${API}/edit`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify({
file: { id: file.id },
config: { instructions: "Fülle das Formular für Erika Mustermann aus, geboren am 12.08.1990 in Musterstadt, wohnhaft Musterstraße 1, 12345 Musterstadt. Unterschreibe mit ihrem Namen und dem heutigen Datum.", advancedOptions: { flattenPdf: true } },
}),
});
const run = await res.json();
if (!res.ok) throw new Error(`${run.code}: ${run.message}`);
if (run.status === "FAILED") throw new Error(`${run.failureReason}: ${run.failureMessage}`);
console.log(run.output.filledValues, run.output.review);
// 3. Herunterladen (Link 15 Minuten gültig)
const pdf = await fetch(run.output.editedFile.presignedUrl);
await writeFile("ausgefuellt.pdf", Buffer.from(await pdf.arrayBuffer()));import os, uuid, requests
API = os.environ["FORMFILL_API_URL"]
AUTH = {"Authorization": f"Bearer {os.environ['FORMFILL_API_KEY']}"}
# 1. PDF hochladen
with open("formular.pdf", "rb") as f:
file = requests.post(f"{API}/files/upload", headers=AUTH,
files={"file": ("formular.pdf", f, "application/pdf")}, timeout=60).json()
# 2. Ausfüllen
res = requests.post(
f"{API}/edit",
headers={**AUTH, "Idempotency-Key": str(uuid.uuid4())},
json={
"file": {"id": file["id"]},
"config": {
"instructions": "Fülle das Formular für Erika Mustermann aus, geboren am 12.08.1990 in Musterstadt, wohnhaft Musterstraße 1, 12345 Musterstadt. Unterschreibe mit ihrem Namen und dem heutigen Datum.",
"advancedOptions": {"flattenPdf": True},
},
},
timeout=300,
)
res.raise_for_status()
run = res.json()
if run["status"] == "FAILED":
raise RuntimeError(f"{run['failureReason']}: {run['failureMessage']}")
# 3. Herunterladen (Link 15 Minuten gültig)
pdf = requests.get(run["output"]["editedFile"]["presignedUrl"], timeout=60)
with open("ausgefuellt.pdf", "wb") as f:
f.write(pdf.content)Ausführlich mit asynchronen Runs und Fehlerbehandlung: Schnellstart in der Dokumentation.
Webhooks mit Signatur
Webhooks richten Sie pro Organisation im Dashboard ein. Ereignisse:
edit_run.processededit_run.failedform_detection_run.processedform_detection_run.failedbatch.completed
Jede Zustellung trägt x-formfill-signature: t=<unix>,v1=<hex> – ein HMAC-SHA256 über "<t>.<body>" mit Ihrem Webhook-Secret – sowie x-formfill-event-id und x-formfill-event-type. Fehlgeschlagene Zustellungen werden mit wachsendem Abstand wiederholt.
import crypto from "node:crypto";
/** Verify x-formfill-signature: t=<unix>,v1=<hex hmac_sha256(secret, "<t>.<body>")> */
export function verifyFormfillSignature(rawBody: string, header: string, secret: string, toleranceSec = 300): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2) as [string, string]));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}import hashlib, hmac, time
def verify_formfill_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Idempotenz, Limits und Schlüssel
- Idempotency-Key
- Senden Sie bei POST-Anfragen einen eindeutigen Idempotency-Key. Wiederholungen mit demselben Schlüssel innerhalb von 24 Stunden liefern die ursprüngliche Antwort – ohne doppelte Abrechnung.
- Ratenbegrenzung
- Bis zu 600 Anfragen pro Minute im Tarif API. Darüber antwortet die API mit 429 und Retry-After.
- Testschlüssel
- ff_test_…: kostenlos, bis zu 50 Seiten pro Tag, Ergebnisse mit Wasserzeichen „TEST“.
- Live-Schlüssel
- ff_live_…: Abrechnung pro Seite. Schlüssel werden nur als Hash gespeichert und einmal angezeigt.
Preise pro Seite
| Seiten pro Monat | Ausfüllen inkl. Erkennung | Ausfüllen mit Schema | Nur Erkennung |
|---|---|---|---|
| 1 – 100 | kostenlos | kostenlos | kostenlos |
| 101 – 10.000 | 0,03 € | 0,01 € | 0,02 € |
| 10.001 – 100.000 | 0,025 € | 0,008 € | 0,015 € |
| ab 100.001 | 0,02 € | 0,006 € | 0,01 € |
Beispiel: 2.000 ausgefüllte Seiten im Monat kosten 57,00 €, 20.000 Seiten 547,00 € (jeweils zzgl. USt.). Mit mitgeliefertem Schema oder gespeicherter Vorlage entfällt die Felderkennung, dann gilt der günstigere Preis „Ausfüllen mit Schema“. Der Tarif Business enthält die API ebenfalls.
Alle PreiseHosting und Datenschutz
- Dateien und Ergebnisse werden in Frankfurt am Main gespeichert und nach der eingestellten Frist automatisch gelöscht; Download-Links gelten 15 Minuten.
- KI-Anfragen laufen ausschließlich in EU-Rechenzentren (Microsoft Azure, EU-Region), an Modell-Endpunkte ohne Datenspeicherung und ohne Training – in jedem Tarif. Einzelheiten und Grenzen auf der Seite Sicherheit.
- Auftragsverarbeitungsvertrag nach Art. 28 DSGVO für alle Geschäftskunden: AVV lesen.