Mit der propform-API kannst du deine Formulare, Einsendungen, Statistiken, PDF-Vorlagen, Briefpapiere, Mitarbeiter und Konto-Einstellungen aus externen Anwendungen heraus verwalten – zum Beispiel aus eigenen Tools, Skripten oder Automatisierungsplattformen wie Zapier, Make oder n8n.
Basis-URL:
https://propform.io/api/v1Alle Anfragen und Antworten sind JSON (
Content-Type: application/json). Zeitstempel sind im ISO-8601-Format (z.B.2026-07-07T08:30:00+00:00).
API-Key erstellen: Gehe zu Einstellungen → API-Keys, klicke auf API-Key erstellen und kopiere den angezeigten Key (Format pf_live_…). Er wird dir aus Sicherheitsgründen nur dieses eine Mal angezeigt.
Verbindung testen: Ersetze pf_live_DEIN_KEY und rufe auf:
curl https://propform.io/api/v1/me \
-H "Authorization: Bearer pf_live_DEIN_KEY"
Antwort:
{ "data": { "id": 42, "name": "Max Makler", "email": "max@makler.de" } }
Erste echte Abfrage – deine Formulare auflisten:
curl https://propform.io/api/v1/forms \
-H "Authorization: Bearer pf_live_DEIN_KEY"
Die API nutzt API-Keys als Bearer-Token. Jede Anfrage braucht den Key im Authorization-Header (Authorization: Bearer pf_live_…). Ohne oder mit ungültigem Key antwortet die API mit 401.
403 und error.code api_key_area_missing (error.area nennt den Bereich, die Meldung den Weg). Ist der Bereich onOffice-Daten für dein Konto nicht freigeschaltet: 403 mit onoffice_data_not_enabled.Bei Überschreitung antwortet die API mit 429 Too Many Requests und einem Retry-After-Header (Sekunden bis zum nächsten erlaubten Versuch). Baue in Automatisierungen ein kurzes Warten ein, wenn du diesen Status siehst.
Erfolgreiche Antworten liefern die Daten im Feld data.
Listen sind paginiert. Steuere sie über per_page (Standard 25, max. 100) und page. Die Antwort enthält zusätzlich links und meta:
{
"data": [ { "id": 14, "internal_title": "Kontaktformular", "active": true } ],
"links": { "first": "...", "last": "...", "prev": null, "next": "...?page=2" },
"meta": { "current_page": 1, "per_page": 25, "total": 63, "last_page": 3 }
}
Fehler haben immer ein message-Feld; bei Validierungsfehlern (422) stehen die Details pro Feld in errors:
{
"message": "The url field is required.",
"errors": { "url": ["The url field is required."] }
}
| Status | Bedeutung |
|---|---|
200 / 201 |
Erfolg (201 = neu angelegt) |
204 |
Erfolg, keine Antwort-Daten (z.B. Löschen) |
401 |
Kein oder ungültiger API-Key |
403 |
Der Datensatz gehört nicht zu deinem Account (oder Funktion nicht im Tarif) |
404 |
Datensatz existiert nicht |
409 |
Aktion aktuell nicht möglich (z.B. onOffice nicht verbunden) |
422 |
Validierungsfehler – Details im Feld errors |
429 |
Rate-Limit erreicht – Retry-After beachten |
| Methode | Endpunkt | Beschreibung |
|---|---|---|
GET |
/forms |
Eigene Formulare auflisten. Filter: active (0/1), template (0/1), updated_since (Datum), folder (Ordner-ID oder none), per_page, page |
POST |
/forms |
Neues Formular anlegen – nur internal_title nötig, startet inaktiv |
GET |
/forms/{id} |
Formular komplett lesen (inkl. Felder, E-Mail- und ChatGPT-Konfiguration) |
PATCH |
/forms/{id} |
Formular konfigurieren – es werden nur die gesendeten Felder geändert; form_template: true (Kopier-Freigabe: jedes propform-Konto mit dem Link kann das Formular kopieren) nur zusammen mit confirm: true |
POST |
/forms/{id}/copy |
Formular kopieren (inkl. Felder, Regeln, E-Mail-Konfiguration und PDF-Vorlage; form-privates Briefpapier wird nicht übernommen); die Kopie startet inaktiv |
POST |
/forms/copy-template |
Freigegebene Vorlage eines anderen Kontos in den eigenen Account kopieren: Body slug (die 12 Zeichen aus dem öffentlichen Link); 403, wenn der Eigentümer sie nicht freigegeben hat; Design und Briefpapier des eigenen Kontos, onOffice-Feldbindungen ungeprüft, Kopie inaktiv |
Das Formular ist in der API eine Ressource: Auch die Einstellungen des E-Mail-Versands und der ChatGPT-Integration liest und schreibst du direkt am Formular. Die Feldnamen entsprechen exakt den Einstellungen im Formular-Editor.
curl -X PATCH https://propform.io/api/v1/forms/123 \
-H "Authorization: Bearer pf_live_DEIN_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Neuer Titel", "active": true}'
Wichtige Besonderheiten:
"active": true) funktioniert nur, solange dein Tarif-Limit aktiver Formulare nicht erreicht ist (sonst 422). Deaktivieren geht immer.slug, Zähler und Vorlagen-Flags. Passwörter (z.B. Formular-Passwort) sind schreibbar, tauchen aber nie in Antworten auf.form_template zeigt die Freigabe an (schreibbar per PATCH, true nur mit confirm: true); mit dem Filter template=0/1 kannst du danach filtern.url des Formulars.Ein Formular hat über 100 Einstellungen (alle, die auch der Editor bietet). Die wichtigsten:
| Feld | Bedeutung |
|---|---|
internal_title |
Interner Name (nur im Dashboard sichtbar) |
title |
Öffentliche Überschrift des Formulars |
custom_slug |
Sprechender URL-Teil (muss in deinem Account eindeutig sein) |
active |
Formular online (true) oder offline (false) |
description |
Einleitungstext über dem Formular |
submit_button_label |
Beschriftung des Absende-Buttons |
thankyou_headline / thankyou_text |
Überschrift und Text der Danke-Seite |
redirect |
Weiterleitungs-URL nach dem Absenden (statt Danke-Seite) |
background_color / accent_color |
Design-Farben (Hex, z.B. #0d6efd) |
subject / body / receiver |
Betreff, Text und Empfänger der Bestätigungs-E-Mail |
send_email_via_onoffice |
Bestätigungs-E-Mail über onOffice versenden |
Das vollständige Objekt siehst du jederzeit über GET /forms/{id}.
👉 Die vollständige Liste aller Felder und Formate findest du in der API-Feldreferenz.
| Methode | Endpunkt | Beschreibung |
|---|---|---|
GET |
/forms/{id}/fields |
Felder eines Formulars (sortiert nach Position) |
POST |
/forms/{id}/fields |
Feld anlegen – onoffice_module ist Pflicht, Position automatisch am Ende |
GET |
/fields/{id} |
Einzelnes Feld lesen |
PATCH |
/fields/{id} |
Feld ändern (nur gesendete Eigenschaften) |
DELETE |
/fields/{id} |
Feld löschen |
PATCH |
/forms/{id}/fields/reorder |
Felder neu sortieren – field_ids muss alle Feld-IDs des Formulars in der gewünschten Reihenfolge enthalten |
curl -X POST https://propform.io/api/v1/forms/123/fields \
-H "Authorization: Bearer pf_live_DEIN_KEY" \
-H "Content-Type: application/json" \
-d '{"onoffice_module": "address", "onoffice_field_name": "Vorname", "label": "Vorname", "required": true}'
| Feld | Bedeutung |
|---|---|
onoffice_module |
Zielmodul: address, estate, task, calendar, agentsLog – oder relationlist für das Verknüpfungs-Feld (Pflicht beim Anlegen) |
onoffice_field_name |
Ziel-Feldname in onOffice (siehe onOffice-Feldkonfiguration) |
label |
Beschriftung des Felds im Formular |
hint / placeholder |
Hilfetext bzw. Platzhalter im Eingabefeld |
required |
Pflichtfeld (true/false) |
hidden / disabled / read_only |
Versteckt / deaktiviert / nur lesbar |
default_value |
Vorbelegter Wert |
half_size |
Feld auf großen Bildschirmen halbbreit darstellen |
datalist |
Auswahlwerte als Semikolon-Liste (Rot;Grün;Blau) |
display_variant |
Darstellungsvariante des Felds (siehe unten) |
select_option_meta |
Zusatzangaben pro Auswahl-Schlüssel für Karten-/Icon-/Bild-Darstellungen (JSON-String, siehe unten) |
Mit display_variant steuerst du, wie ein Feld im Formular dargestellt wird — der gespeicherte Wert und die onOffice-Anbindung bleiben identisch. null bzw. leer = Standard-Darstellung. Gültige Werte je Feldtyp:
| onOffice-Feldtyp | Werte |
|---|---|
singleselect / user |
radio, buttons, cards, icons, images, likert |
multiselect |
checkboxes, chips, icons, images |
boolean |
checkbox, yesno |
integer |
slider, stars, nps |
float |
slider, stars, nps |
date |
calendar |
Von/Bis-Suchkriterium (integer/float im Modul searchcriteria) |
rangeslider |
Unbekannte oder zum Feldtyp unpassende Werte werden beim Rendern ignoriert (Standard-Darstellung). Für cards, icons und images liefert select_option_meta die Zusatzangaben pro onOffice-Schlüsselwert — ein JSON-String der Form:
{"haus": {"icon": "home", "description": "Freistehend oder Reihenhaus"}, "wohnung": {"image": "https://…/wohnung.jpg"}}
icon ist ein Icon-Name aus dem integrierten Tabler-Icons-Set (nur Kleinbuchstaben/Ziffern/Bindestriche, z.B. home, building-estate, currency-euro), image eine http(s)-Bild-URL (idealerweise aus der Medienbibliothek), description ein Text unter dem Options-Titel (Karten, Icon- und Bild-Kacheln). Icons erscheinen bei Icon-Kacheln, vor dem Options-Label der Button-Gruppe und über den Buttons der Likert-Skala. Leere bzw. unbekannte Angaben werden ignoriert. Beim Schieberegler kommen Minimal-/Maximal-Wert und Intervall aus min_value/max_value/step (auch beim Von/Bis-rangeslider); bei der Bewertung bestimmt max_value die Anzahl der Stufen (2–10, Standard 5).
Einstellungsoptionen je Darstellung (alle optional, unpassende Werte werden beim Rendern ignoriert):
| Eigenschaft | Wirkt bei | Bedeutung |
|---|---|---|
variant_axis_labels |
slider |
Eigene Beschriftungen unter dem Regler, Semikolon-getrennt (z.B. günstig;mittel;teuer); Vorrang vor datalist |
slider_allow_input |
slider, rangeslider |
true = Wert zusätzlich als Zahlenfeld eintippbar |
range_default_von / range_default_bis |
rangeslider + Standard-Von/Bis |
Belegt Von und Bis getrennt vor (Vorrang vor default_value, der beide Seiten gleich füllt); beim Slider werden Voreinstellungen — anders als unberührte Regler — mitgesendet |
scale_label_left / scale_label_right |
stars, nps, likert |
Beschriftung der Skalen-Endpunkte |
rating_symbol |
stars |
stars (Standard), hearts, smileys (Stimmungs-Skala, 2–5 Stufen; darüber Fallback auf Sterne) oder custom (eigenes Icon via rating_custom_icon) |
rating_custom_icon |
stars |
Tabler-Icon-Name für rating_symbol: "custom" (ungültig/leer ⇒ Sterne) |
variant_tile_size |
cards, icons, images |
compact oder large (leer = normal) |
variant_image_ratio |
images |
square oder 16x9 (leer = 4:3) |
variant_image_fit |
images |
contain = ganzes Bild einpassen (leer = füllen/zuschneiden) |
multiselect_min_selected / multiselect_max_selected |
Mehrfachauswahl-Darstellungen | Mindest-/Höchstanzahl wählbarer Optionen (Browser-seitig erzwungen) |
calendar_date_mode |
calendar |
future (nur ab heute) oder past (nur bis heute) |
calendar_min_date / calendar_max_date |
calendar |
Festes frühestes/spätestes Datum (YYYY-MM-DD); Vorrang vor calendar_date_mode |
calendar_disable_weekends |
calendar |
true = Wochenenden sperren |
yesno_yes_label / yesno_no_label |
yesno |
Eigene Button-Beschriftungen (leer = „Ja"/„Nein" in der Account-Sprache) |
yesno_icons |
yesno |
true = Icons auf den Buttons (Standard Haken/Kreuz) |
yesno_yes_icon / yesno_no_icon |
yesno |
Eigene Tabler-Icons statt Haken/Kreuz (wirken nur mit yesno_icons: true) |
variant_columns |
radio, checkboxes |
Listen-Spalten: 1–3 (mobil immer einspaltig) |
variant_centered |
Buttons/Kacheln/Chips/yesno/stars/nps/calendar/Slider |
true = zentriert statt linksbündig (bei Slidern das Eintipp-Feld) |
variant_icon_color |
icons, yesno, buttons, likert |
Eigene Icon-Farbe als Hex (#RRGGBB), leer = erbt die Textfarbe |
Hinweis Mehrfachauswahl: Die Kachel-/Listen-Darstellungen zeigen die Optionen flach, eine Eltern-Kind-Automatik gibt es dort nicht. Der gespeicherte
multiselect_modebleibt beim Setzen einer Darstellungsvariante unverändert und wirkt wieder, sobald du auf die Standard-Darstellung zurückstellst.
Hinweis Slider: Der einfache Schieberegler sendet — wie der Von/Bis-Slider — erst nach einer Interaktion einen Wert; ein unberührter Slider verhält sich wie ein leeres Feld.
Hinweis: Der ältere Range-Slider über
special_field: "range"funktioniert unverändert weiter (Legacy). Für neue Slider empfehlen wirdisplay_variant: "slider"— hat ein Feld beides gesetzt, gewinntdisplay_variant.
Das Verknüpfungs-Feld (verbundene onOffice-Datensätze anzeigen, verbinden, anlegen, bearbeiten, trennen) legst du mit onoffice_module: "relationlist" an. Der Relationstyp besteht aus relation_type (z.B. estate:address:owner) plus relation_anchor_module (estate oder address – welche Seite ist der geladene Formular-Datensatz); dazu kommen die Schalter relation_show_existing, relation_allow_search, relation_allow_create, relation_allow_edit, relation_allow_remove, der Dublettencheck (relation_duplicate_check + relation_duplicate_check_fields) und die Miniformular-Elementliste relation_subform_elements (Array aus {type: "field", name, required, half} bzw. Layout-Elementen {type: "headline"|"description"|"dividingline"|"collapsible", text}). Für die Neuanlage zusätzlich relation_advisor_mode (anchor = Betreuer des geladenen Datensatzes übernehmen, fixed = fester Benutzer aus relation_advisor_user_id) und relation_auto_open_create (Miniformular ohne angezeigte Verbindungen automatisch aufklappen). Alle Werte und Grenzen stehen in der Feld-Referenz.
curl -X POST https://propform.io/api/v1/forms/123/fields \
-H "Authorization: Bearer pf_live_DEIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"onoffice_module": "relationlist",
"relation_type": "estate:address:owner",
"relation_anchor_module": "estate",
"label": "Eigentümer",
"relation_show_existing": true,
"relation_allow_create": true,
"relation_subform_elements": [
{"type": "field", "name": "Vorname", "half": true},
{"type": "field", "name": "Name", "required": true, "half": true},
{"type": "field", "name": "Email", "required": true}
]
}'
Sortieren: Beim reorder bestimmt die Reihenfolge im field_ids-Array die neue Position. Das Array muss exakt alle Feld-IDs des Formulars enthalten (fehlt eine oder ist eine fremde dabei → 422):
curl -X PATCH https://propform.io/api/v1/forms/123/fields/reorder \
-H "Authorization: Bearer pf_live_DEIN_KEY" \
-H "Content-Type: application/json" \
-d '{"field_ids": [4712, 4711, 4713]}'
GET /onoffice/fields liefert die Feldkonfiguration deines onOffice-Accounts – pro Modul (address, estate, task, calendar, agentsLog, file, project) alle Felder mit Label, Typ, Länge und erlaubten Werten. So findest du die gültigen Werte für onoffice_field_name. Mit ?module=address filterst du auf ein Modul.
{
"data": {
"address": {
"Vorname": { "label": "Vorname", "type": "varchar", "length": 50, "default": null, "permittedvalues": null },
"Anrede": { "label": "Anrede", "type": "singleselect", "length": null, "default": null,
"permittedvalues": { "HERR": "Herr", "FRAU": "Frau" } }
}
}
}
Voraussetzung ist eine funktionierende onOffice-API-Anbindung; andernfalls antwortet der Endpunkt mit
409.
POST /onoffice/fields/refresh lädt die Feldkonfiguration neu von onOffice – z.B. nachdem du in onOffice ein Feld angelegt oder Auswahlwerte geändert hast. Der Abruf läuft im Hintergrund: Die Antwort ist 202 mit data.status = pending, wenige Sekunden später liefert GET /onoffice/fields den neuen Stand. Beide Endpunkte geben in meta den Zeitpunkt des letzten Aufbaus (updated_at), die Sprache der Feldlabels (locale) und refresh_pending zurück (true, solange ein per API angestoßener Abruf noch läuft). Ohne diesen Aufruf aktualisiert propform die Konfiguration einmal täglich sowie bei jedem Öffnen der Formularübersicht. Limit: 120 Aufrufe pro Stunde.
{
"message": "…",
"data": { "status": "pending", "already_pending": false },
"meta": { "updated_at": "2026-09-08T06:12:41+02:00", "locale": "de", "refresh_pending": true, "last_refresh_failed": false }
}
| Methode | Endpunkt | Beschreibung |
|---|---|---|
GET |
/forms/{id}/rules |
Regeln eines Formulars (sortiert nach Priorität) |
PUT |
/forms/{id}/rules |
Alle Regeln des Formulars ersetzen (leeres Array = alle löschen) |
Der Schreib-Endpunkt arbeitet wie der Bedingungs-Editor im Dashboard: Es werden immer alle Regeln auf einmal gespeichert (kein einzelnes Hinzufügen). Lies also erst mit GET, ändere die Liste und schicke sie komplett per PUT zurück. Eine Regel besteht aus clauses (Wenn-Bedingungen, verknüpft mit mode = all/any) und actions (Dann-Aktionen):
curl -X PUT https://propform.io/api/v1/forms/123/rules \
-H "Authorization: Bearer pf_live_DEIN_KEY" \
-H "Content-Type: application/json" \
-d '{"rules": [{
"title": "Zusatzfeld einblenden",
"mode": "all", "priority": 10, "is_enabled": 1, "run_on_init": 1,
"clauses": [{"when_field_id": "4711", "operator": "equals", "value": "Ja"}],
"actions": [{"order": 10, "action": "show", "target_field_id": "4712"}]
}]}'
equals, not_equals, empty, not_empty, starts_with, not_starts_with, contains, not_containsshow, hide, enable, disable, require, optional, hide_option, unhide_option, set_value, set_label, make_readonly, unmake_readonly, enable_rules_by_tag, disable_rules_by_tag__form_title, __form_description, __submit, __submit_spinner, __multistep_back, __multistep_next (dort nur set_label, show, hide; beim Spinner nur set_label).403).Details zur Funktionsweise der Regeln findest du unter Bedingungen, Regeln & Berechnungen.
| Methode | Endpunkt | Beschreibung |
|---|---|---|
GET |
/forms/{id}/submissions |
Einsendungen eines Formulars. Filter: created_after, created_before, sort (asc/desc), per_page |
GET |
/submissions/{id} |
Einzelne Einsendung |
GET |
/forms/{id}/stats |
Aufrufe, Einsendungen und Conversion-Rate; optional from/to (Datum) |
Der Inhalt einer Einsendung (content) ist nach onOffice-Modulen gruppiert:
{
"data": {
"id": 456, "form_id": 123, "index": 42,
"content": { "address": { "Vorname": "Max", "Name": "Mustermann" }, "estate": { "Id": "131" } },
"ip_address": "203.0.113.7",
"created_at": "2026-07-07T08:30:00+00:00"
}
}
Hast du in den Datenschutz-Einstellungen das Speichern von Einsendungsdaten deaktiviert, ist content immer null – die Einsendung zählt aber trotzdem für die Statistik. Die stats-Antwort enthält views, submissions und conversion_rate.
Statt Einsendungen regelmäßig abzufragen, kannst du dich per Webhook benachrichtigen lassen: propform schickt bei jeder neuen Einsendung sofort einen POST an deine URL.
💡 Webhooks kannst du auch ohne API-Key im Frontend verwalten: in den Einstellungen unter „Webhooks" (für alle Formulare) oder direkt im Formular-Editor unter „Webhooks (Zapier, n8n, Make …)".
| Methode | Endpunkt | Beschreibung |
|---|---|---|
GET |
/webhooks |
Deine Webhook-Abos auflisten |
POST |
/webhooks |
Abo anlegen: url (Pflicht), form_id (optional – ohne Angabe gilt das Abo für alle deine Formulare), payload_template (optional – siehe unten), message_key (optional – Feldname der aufgelösten Nachricht, Standard text) |
DELETE |
/webhooks/{id} |
Abo löschen |
Anforderungen an die URL: Sie muss öffentlich erreichbar sein und http:// oder https:// verwenden. Interne oder lokale Adressen (z.B. localhost, 127.0.0.1, private Netz-IPs) werden aus Sicherheitsgründen abgelehnt (422). Da die Einsendungsdaten übertragen werden, verwende möglichst https://.
Die Antwort beim Anlegen enthält das secret, mit dem jede Zustellung signiert wird (du kannst es später jederzeit über GET /webhooks wieder abrufen):
{
"data": {
"id": 7, "event": "submission.created",
"url": "https://example.com/hook", "form_id": 123,
"secret": "a1b2c3…", "payload_template": null, "message_key": "text", "is_active": true,
"failure_count": 0, "last_success_at": null, "last_failure_at": null,
"created_at": "2026-07-07T08:30:00+00:00"
}
}
Jede Zustellung enthält zwei Header:
X-Propform-Event – der Event-Typ (aktuell immer submission.created)X-Propform-Signature – sha256=<HMAC-SHA256 des Request-Bodys mit deinem secret>Beispiel-Payload:
{
"event": "submission.created",
"created_at": "2026-07-07T08:30:00+00:00",
"form": { "id": 123, "slug": "aB3x…", "custom_slug": "kontakt", "title": "…", "internal_title": "…" },
"submission": { "id": 456, "index": 42, "content": { "address": { "Vorname": "Max" } } },
"onoffice_ids": { "address": { "ID": "789" } }
}
payload_template)Optional kannst du pro Abo ein Freitext-Template hinterlegen. propform löst darin nach jeder Formularabsendung onOffice-Makros auf (gegen die Datensätze, die die Einsendung angelegt oder bearbeitet hat) und schickt das Ergebnis als eigenes Nachrichtenfeld im Payload mit — ideal als fertige Nachricht für Slack & Co., ohne dass du im Empfänger-Workflow etwas zusammenbauen musst. Der Feldname ist standardmäßig text (siehe unten).
Beispiel-Template:
Neue Anfrage von _Vorname _Name zu _objekttitel → _getEstateLink
wird im Payload zu:
"text": "Neue Anfrage von Max Mustermann zu Traumwohnung am Stadtpark → https://smart.onoffice.de/…"
Feldname der Nachricht (message_key): Standardmäßig heißt das Feld text – damit verstehen Slack, Microsoft Teams und Google Chat den Payload direkt als Nachricht. Discord erwartet content. Für Zapier/n8n/Make ist der Name beliebig. Du kannst ihn pro Webhook über den Parameter message_key (im Frontend über „Feldname der Nachricht") anpassen.
Zu beachten:
_getAddressLink, _getEstateLink, …)._objekttitel eine verknüpfte Immobilie). Ohne Kontext bleibt das Makro unaufgelöst im Text stehen.Prüfe die Signatur, um sicherzustellen, dass die Anfrage wirklich von propform stammt. Berechne den HMAC über den rohen Request-Body (nicht über das geparste JSON) und vergleiche ihn zeitkonstant.
PHP:
$secret = 'DEIN_WEBHOOK_SECRET';
$body = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_PROPFORM_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $body, $secret);
if (!hash_equals($expected, $signature)) {
http_response_code(403);
exit('Ungültige Signatur');
}
// ab hier: $body ist echt – json_decode($body, true) verarbeiten
Node.js (Express):
const crypto = require('crypto');
// WICHTIG: express.raw() nutzen, damit der Body unverändert bleibt
app.post('/hook', express.raw({ type: 'application/json' }), (req, res) => {
const secret = process.env.PROPFORM_WEBHOOK_SECRET;
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(req.body).digest('hex');
const got = req.header('X-Propform-Signature') || '';
if (expected.length !== got.length ||
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got))) {
return res.status(403).send('Ungültige Signatur');
}
const payload = JSON.parse(req.body.toString());
res.sendStatus(200);
});
2xx-Status antworten. Bei Fehlern versucht propform die Zustellung bis zu 3-mal (mit Wartezeit).is_active: false, sichtbar über GET /webhooks). Lege es danach einfach neu an.Ordner strukturieren die Formularübersicht im Dashboard. Jedes Formular liegt in maximal einem Ordner (form_folder_id, null = „Ohne Ordner"). Ordner können eine Unterebene haben (parent_id, maximal zwei Ebenen). Nicht zu verwechseln mit den Formular-Gruppen, die die Kopier-Freigabe steuern.
| Methode | Endpunkt | Beschreibung |
|---|---|---|
GET |
/form-folders |
Deine Ordner mit IDs und Formular-Anzahl |
POST |
/form-folders |
Ordner anlegen – name nötig (max. 100 Zeichen), optional parent_id (ID eines Hauptordners → legt einen Unterordner an) |
PATCH |
/form-folders/{id} |
Ordner umbenennen (name) und/oder umhängen (parent_id: ID eines Hauptordners oder null = Hauptordner) |
DELETE |
/form-folders/{id} |
Ordner löschen – die Formulare bleiben bestehen und landen wieder unter „Ohne Ordner", Unterordner werden zu Hauptordnern |
Die Zuordnung setzt du im Formular-PATCH über form_folder_id (ID eines eigenen Ordners oder null). In der Formular-Liste filterst du mit ?folder=<id> bzw. ?folder=none.
| Methode | Endpunkt | Beschreibung |
|---|---|---|
GET |
/form-groups |
Deine Gruppen mit IDs, copy-Flag und Formular-Anzahl |
POST |
/form-groups |
Gruppe anlegen – nur name nötig |
PATCH |
/form-groups/{id} |
name und/oder copy ändern (Kopierbarkeit der Formulare dieser Gruppe) |
DELETE |
/form-groups/{id} |
Gruppe löschen – die Formulare bleiben bestehen und verlieren nur die Gruppenzuordnung |
Die Gruppen-IDs weist du im Formular-PATCH als form_groups (Array) zu.
propform kann aus jeder Einsendung ein eigenes PDF erzeugen (siehe PDF-Generator). Die Vorlage eines Formulars – Blockliste, Design und Einstellungen – liest und schreibst du komplett über die API:
| Methode | Endpunkt | Beschreibung |
|---|---|---|
GET |
/forms/{id}/pdf-template |
Vorlage lesen (enabled, name, blocks, design, settings); existiert noch keine, wird sie automatisch aus den Formularfeldern vorgeschlagen |
PATCH |
/forms/{id}/pdf-template |
Teilupdate – nur die gesendeten Teile werden geändert |
POST |
/forms/{id}/pdf-template/regenerate |
Blockliste aus dem aktuellen Feldbestand neu erzeugen (Design, Einstellungen und Aktiv-Status bleiben) |
GET |
/forms/{id}/pdf-template/preview |
Echte PDF-Vorschau mit Beispieldaten (application/pdf; max. 10 Abrufe/Minute) |
GET |
/account/pdf-design |
Kontoweites Design-Preset lesen (design + settings) |
PATCH |
/account/pdf-design |
Design-Preset ändern – neue Formular-Vorlagen erben es als Startpunkt |
curl -X PATCH https://propform.io/api/v1/forms/123/pdf-template \
-H "Authorization: Bearer pf_live_DEIN_KEY" \
-H "Content-Type: application/json" \
-d '{"enabled": true, "design": {"accent_color": "#0d6efd"}}'
Wichtige Besonderheiten:
regenerate (ersetzt eigene Block-Anpassungen!) oder ergänze die Blöcke gezielt per PATCH.settings.document_signing.enabled ein, sorgt propform automatisch für einen Dokument-Unterschriftsblock in der Vorlage (falls keiner vorhanden ist).blocks ist ein Array aus Blöcken. Jeder Block hat type, eine automatisch vergebene id und typabhängige Eigenschaften (max. 300 Blöcke pro Vorlage):
| Typ | Eigenschaften |
|---|---|
heading |
text (max. 500 Zeichen), level (1, 2 oder 3) |
paragraph |
text (max. 5.000 Zeichen) |
field |
Einzelnes Feld mit Wert: field_id (Feld dieses Formulars) oder onoffice (Referenz, siehe unten), optional label |
fieldlist |
Beschriftungs-Wert-Liste: field_ids = Array aus Feld-IDs und/oder onOffice-Referenzen |
columns |
Zweispaltig: left und right sind jeweils Block-Arrays (eine Ebene tief) |
image |
url (http/https), width_percent (10–100), align (left/center/right), caption |
divider |
style: line (Linie) oder space (Abstand) |
pagebreak |
Seitenumbruch, keine Eigenschaften |
signature |
field_id = Unterschriften-Feld des Formulars oder null = Dokument-Unterschrift nach dem Absenden; label; required (nur bei Dokument-Unterschrift) |
onOffice-Referenzen: In field/fieldlist kannst du statt einer Feld-ID auch ein Feld des verknüpften onOffice-Datensatzes ausgeben: {"module": "address", "name": "Telefon1", "label": "Telefon"} (Module address und estate; das Feld muss in deiner onOffice-Feldkonfiguration existieren).
Beispiel:
{
"blocks": [
{"type": "heading", "text": "Mieterselbstauskunft", "level": 1},
{"type": "paragraph", "text": "Angaben des Interessenten:"},
{"type": "fieldlist", "field_ids": [4711, 4712, {"module": "estate", "name": "objekttitel", "label": "Objekt"}]},
{"type": "pagebreak"},
{"type": "signature", "field_id": null, "label": "Ort, Datum, Unterschrift", "required": true}
]
}
Design-Schlüssel (design): font, heading_font, font_size (7–14), text_color/heading_color/accent_color (Hex), logo_url, logo_position (left/center/right), logo_width_mm (10–90), label_column_width (20–60), header_text, footer_text, show_page_numbers, letterhead_id (siehe Briefpapiere) sowie letterhead_margin_top/right/bottom/left (0–100 mm, null = Ränder des Briefpapiers).
Einstellungs-Schlüssel (settings): empty_fields (hide/dash/blank), embed_uploaded_images, attach_uploaded_pdfs, embed_existing_files, embedded_image_size (small/column/full), document_signing.enabled.
Eigenes Briefpapier (PDF oder Bild) wird als Hintergrund unter jede Seite des erzeugten PDFs gelegt – mit optional eigenem Folgebogen ab Seite 2:
| Methode | Endpunkt | Beschreibung |
|---|---|---|
GET |
/letterheads |
Deine Briefpapiere; mit ?form_id= genau die in diesem Formular wählbaren |
POST |
/letterheads |
Hochladen (multipart/form-data, siehe unten) |
PATCH |
/letterheads/{id} |
letterhead_name und Ränder ändern |
DELETE |
/letterheads/{id} |
Löschen – Vorlagen, die es nutzen, rendern danach wieder im Standard-Layout |
Upload-Parameter (POST, multipart): letterhead_name (Pflicht), letterhead_first (Pflicht, PDF/PNG/JPG bis 10 MB – der Erstbogen), letterhead_continuation (optional – der Folgebogen, gleicher Dateityp), letterhead_margin_top/right/bottom/left (optional, mm), form_id (optional – damit wird das Briefpapier formprivat, ohne landet es in der Konto-Bibliothek).
curl -X POST https://propform.io/api/v1/letterheads \
-H "Authorization: Bearer pf_live_DEIN_KEY" \
-F "letterhead_name=Firmenbogen" \
-F "letterhead_first=@briefbogen.pdf" \
-F "letterhead_continuation=@folgebogen.pdf"
422 abgelehnt. Bilder mit niedriger Auflösung werden angenommen, die Antwort enthält dann aber einen Hinweis im Feld warning.design.letterhead_id im PDF-Vorlagen-PATCH setzen (Konto-Briefpapiere überall, formprivate nur im eigenen Formular).| Methode | Endpunkt | Beschreibung |
|---|---|---|
GET |
/account |
Konto-Einstellungen gruppiert lesen |
PATCH |
/account |
Teilupdate – nur die gesendeten Gruppen/Felder werden geändert |
Die Antwort ist nach den Einstellungs-Bereichen des Dashboards gruppiert:
| Gruppe | Inhalt | Schreibbar |
|---|---|---|
profile |
name (Profil-/Firmenname) |
✅ |
styling |
Alle default_*-Design-Standards für neue Formulare (Farben, Fonts, Logo, Karten-Optik …) |
✅ |
imprint |
Standard-Impressum: default_privacypolicy_url, default_imprint_url, default_homepage_url, default_imprint_line_1 bis _5 |
✅ |
notifications |
Bis zu 3 Empfänger technischer Benachrichtigungen (notification_email_1–3) + notification_self |
✅ |
statistics |
save_submission_data, save_ips (Datenschutz) |
✅ |
domain |
username (Subdomain), externaldomain, externaldomain_active |
❌ nur lesbar |
staff_area |
enabled, session_days (1–90), seat_limit + active_members (nur lesbar) |
teilweise |
curl -X PATCH https://propform.io/api/v1/account \
-H "Authorization: Bearer pf_live_DEIN_KEY" \
-H "Content-Type: application/json" \
-d '{"styling": {"default_accent_color": "#0d6efd"}, "statistics": {"save_ips": false}}'
domain ist bewusst nur lesbar: Ein Wechsel der Subdomain oder Domain würde alle bereits eingebetteten Formular-Links brechen – das geht nur im Dashboard.default_google_font_name, default_headline_font): Erlaubt sind nur die Namen, die die Feldreferenz bei google_font_name bzw. headline_font als Auswahl nennt – ein anderer Name wird mit 422 abgelehnt.staff_area.enabled lässt sich nur aktivieren, wenn dein Tarif Mitarbeiter-Plätze enthält (seat_limit > 0, sonst 422). seat_limit selbst ergibt sich aus dem Tarif und ist nicht schreibbar.notifications werden als kompakte Liste gespeichert: Lücken rücken nach dem Speichern auf.| Methode | Endpunkt | Beschreibung |
|---|---|---|
GET |
/account/onoffice |
Verbindungsstatus und Einstellungen der onOffice-Anbindung |
PATCH |
/account/onoffice |
Einstellungen ändern (max. 5 Anfragen/Minute) |
Die Antwort enthält u.a. api_type (normal/marketplace_api), connection_ok, credentials_set, email_onoffice_api_user (+ _is_functional), field_config_locale und reason_cancellation. API-Token und -Secret tauchen aus Sicherheitsgründen in keiner Antwort auf.
Per PATCH änderbar:
| Feld | Bedeutung |
|---|---|
api_type |
normal (eigene onOffice-API-Zugangsdaten) oder marketplace_api (Marketplace-Anbindung; erfordert einen verbundenen Marketplace-Benutzer) |
email_onoffice_api_user |
E-Mail-Identitäten des API-Benutzers als Array (max. 10) – jede Adresse wird per Testversand geprüft |
email_onoffice_marketplace_api_user |
dito für die Marketplace-Anbindung |
field_config_locale |
Sprache der onOffice-Feldlabels (de, en, …) – der Feldkonfigurations-Cache wird danach im Hintergrund neu aufgebaut |
reason_cancellation |
Eigene Absagegründe als Array (max. 50 Einträge, je max. 40 Zeichen; leer = Standardliste) |
Formulare mit aktiviertem Mitarbeiter-Zugangsschutz sind nur nach Login per Magic-Link erreichbar. Die Mitarbeiter und Gruppen dahinter verwaltest du per API:
| Methode | Endpunkt | Beschreibung |
|---|---|---|
GET |
/staff-members |
Mitarbeiter inkl. Gruppenzuordnung, source (onoffice/manual) und is_active |
POST |
/staff-members |
Externen Mitarbeiter anlegen: name, email, optional groups (Array von Gruppen-IDs) |
PATCH |
/staff-members/{id} |
Teilupdate: name, email (nur manuelle), groups, is_active |
DELETE |
/staff-members/{id} |
Mitarbeiter löschen (beendet auch laufende Sessions) |
POST |
/staff-members/import-onoffice |
onOffice-Benutzer importieren: onoffice_user_ids (Array, optional – ohne Angabe alle importierbaren). Antwort: created, updated |
GET |
/staff-groups |
Gruppen inkl. Mitgliederzahl (onoffice_group_id gesetzt = aus onOffice gespiegelt) |
POST |
/staff-groups |
Lokale Gruppe anlegen (name) |
DELETE |
/staff-groups/{id} |
Gruppe löschen |
staff_area.seat_limit unter /account) wird beim Anlegen, Aktivieren und Importieren hart durchgesetzt (422). Nur aktive Mitarbeiter belegen einen Platz.is_active: false) beendet sofort alle laufenden Sessions des Mitarbeiters.staff_only_enabled, staff_allowed_member_ids und staff_allowed_group_ids im Formular-PATCH; die Konto-Schalter (staff_area.enabled, session_days) liegen unter /account.Suchprofile (Suchkriterien) deiner onOffice-Kontakte liest und pflegst du über die offizielle onOffice-API deines Kontos. Voraussetzung sind hinterlegte onOffice-API-Zugangsdaten (sonst 409). Außerdem muss propform die onOffice-Datenwerkzeuge für dein Konto freigeschaltet haben und der API-Key den Bereich onOffice-Daten tragen (sonst 403, siehe Authentifizierung).
| Methode | Endpunkt | Beschreibung |
|---|---|---|
GET |
/onoffice/search-profiles/fields |
Aktive Suchkriterien (Satz „Kein Template“) mit Typ (kind) und Auswahlschlüsseln; ?fresh=1 liest live statt aus dem Feld-Cache |
GET |
/onoffice/addresses/{address}/search-profiles |
Suchprofile einer Adresse (interne Adress-Id); limit (max. 100), offset |
GET |
/onoffice/search-profiles?customer_number=… |
Dasselbe über die Kundennummer |
GET |
/onoffice/search-profiles/{id} |
Ein Suchprofil vollständig |
POST |
/onoffice/addresses/{address}/search-profiles |
Suchprofil anlegen: values, radius, note, public_note, advisor, expiry_date, ko, active (Standard true) – Antwort 201 |
PATCH |
/onoffice/search-profiles/{id} |
Nur die gesendeten Angaben ändern; nur active = aktivieren/deaktivieren |
DELETE |
/onoffice/search-profiles/{id} |
Endgültig löschen – nur mit confirm=true |
POST |
/onoffice/search-profiles/match |
Passende Suchprofile: searchdata (Feld → Wert, wie bei einer Immobilie) und/oder estate_id; limit (max. 50), offset, count_only, group_by_address (Standard true) |
POST /api/v1/onoffice/addresses/3415/search-profiles
{
"values": {"vermarktungsart": "kauf", "objektart": "haus", "kaufpreis": {"from": 200000, "to": 450000}},
"radius": {"city": "Lübeck", "km": 30},
"public_note": "Sucht ein Haus mit Garten"
}
{"from", "to"} (eine Seite darf fehlen, 0 = keine Grenze), Auswahlfelder über den Schlüssel, objekttyp und regionaler_zusatz als Liste; der Umkreis steht unter radius (km, zip, city, street, house_number, country).422, ohne onOffice zu ändern.ko (Liste von Feldnamen aktiver Suchkriterien, dazu Umkreis) ersetzt die KO-Kriterien des Profils, [] leert sie; unbekannte Namen → 422. Antworten nennen sie unter ko.{from, to} (nicht gesetzte Seite null), den Umkreis unter radius und die öffentliche Bemerkung als public_note. Die interne Bemerkung (note) und das Ablaufdatum (expiry_date) gibt die onOffice-API nicht zurück – beide sind nur schreibbar (write_only nennt sie), und note ersetzt eine vorhandene Bemerkung ungesehen.502 mit message und error.onoffice_errorcode; unbekanntes Profil, unbekannte Adresse oder Immobilie → 404.502 mit onoffice_errorcode 156; bei Marketplace-Verbindungen hilft der propform-Support). Ohne advisor trägt onOffice den API-Benutzer als Betreuer ein.Dateien deiner Immobilien und Adressen liest, lädst hoch, änderst und löschst du über die offizielle onOffice-API deines Kontos. {module} ist estates oder addresses; eine Datei-Id gilt nur zusammen mit ihrem Datensatz. Voraussetzung sind hinterlegte onOffice-API-Zugangsdaten (sonst 409). Außerdem muss propform die onOffice-Datenwerkzeuge für dein Konto freigeschaltet haben und der API-Key den Bereich onOffice-Daten tragen (sonst 403, siehe Authentifizierung).
| Methode | Endpunkt | Beschreibung |
|---|---|---|
GET |
/onoffice/files/meta |
Dateiarten (type) und Dokumentmerkmale deines Kontos; was je Modul geht |
GET |
/onoffice/{module}/{record}/files |
Dateien eines Datensatzes; limit (max. 100), offset. Bei Immobilien mit Titel, Art, Dokumentmerkmal, Position und Veröffentlichung |
GET |
/onoffice/{module}/{record}/files/{file} |
Metadaten einer Datei |
GET |
/onoffice/{module}/{record}/files/{file}/content |
Inhalt als Datei (binär, bis 25 MB; Kopfzeile X-File-Sha256) |
POST |
/onoffice/{module}/{record}/files |
Hochladen – genau eine Quelle: Multipart-Feld file (bis 128 MB), content_base64 + filename (bis 20 MB) oder source_url (öffentliche https-Adresse, bis 20 MB). Bei Immobilien zusätzlich title, type, free_text, document_attribute, watermark, position (0 = erste Stelle), publish (nur mit confirm=true) – Antwort 201 |
POST |
/onoffice/estates/{record}/files/link |
Link-Datei: url, type (Link, Ogulo-Link, Film-Link, Objekt-Link), title, free_text – nur Immobilien |
PATCH |
/onoffice/estates/{record}/files/{file} |
Nur die gesendeten Angaben ändern: title, type, free_text, document_attribute ("" entfernt es; title/free_text leert "") – nur Immobilien |
DELETE |
/onoffice/{module}/{record}/files/{file} |
Endgültig löschen – nur mit confirm=true |
curl -X POST https://propform.io/api/v1/onoffice/estates/3813/files \
-H "Authorization: Bearer DEIN_API_KEY" \
-F "file=@grundriss-eg.png" -F "type=Grundriss" -F "title=Grundriss EG"
title, type, free_text und document_attribute beantwortet propform mit 422, ebenso Ändern und Link-Dateien. Hochladen, lesen und löschen gehen.publish=true (mit confirm=true) wendet die Standard-Freigaben deines Kontos an. Fotos und Titelbilder führt onOffice trotzdem als auf der Homepage veröffentlicht (onOffice-Standard für Bilder) – die Antwort zeigt den tatsächlichen Stand (published_on_homepage, publication_status). Nachträglich lassen sich Veröffentlichung und Position über die API nicht ändern.document_attribute_moved_from). Ein neues Titelbild macht das bisherige zum Foto – ohne Bestätigung, die Antwort nennt es (previous_title_image); hat die Immobilie keines, macht onOffice das erste hochgeladene Bild selbst zum Titelbild – die Antwort weist mit einer Warnung darauf hin (das Bild ist dann auf der Homepage sichtbar). Soll eine Datei nicht veröffentlicht werden, lade sie mit type=Dokument hoch: onOffice führt sie als intern (category internal), nicht auf der Homepage und ohne Portale; Bilder führt onOffice als external und auf der Homepage (gemessen).docm, xlsm, pptm …), kein SVG/HTML; Inhalt passend zur Endung (PDF, JPG/PNG/GIF/WEBP, DOCX/XLSX/PPTX); Dateiart passend zum Inhalt (Bild-Arten nur für Bilder, auch beim Ändern); Dokumentmerkmal als Schlüssel aus meta; nur Zeichen bis U+FFFF in Namen, Titel und Freitext. source_url lädt propform selbst, erst nachdem Zugang und Datensatz geprüft sind – nur öffentliche https-Adressen: propform löst den Namen einmal auf, prüft jede Adresse und verbindet sich nur mit diesen; höchstens 3 Weiterleitungen (jede ebenso geprüft), 60 Sekunden.502 mit message und error.onoffice_errorcode; unbekannte Datei oder unbekannter Datensatz → 404.GET /forms/{id}/submissions?sort=desc und die höchste bereits verarbeitete id merken.GET /me.GET /forms?updated_since=….429 ein kurzes Warten einbauen und den Retry-After-Header respektieren.Die aktuelle Version ist v1 und steht fest im Pfad (/api/v1). Wir fügen abwärtskompatibel neue Felder und Endpunkte hinzu, ohne die Version zu ändern – dein Client sollte unbekannte Felder in Antworten daher einfach ignorieren. Grundlegende, nicht abwärtskompatible Änderungen würden unter einer neuen Version (/api/v2) erscheinen.