propform-API (REST)

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/v1

Alle Anfragen und Antworten sind JSON (Content-Type: application/json). Zeitstempel sind im ISO-8601-Format (z.B. 2026-07-07T08:30:00+00:00).

Schnellstart in 3 Schritten

  1. 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.

  2. 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" } }
    
  3. Erste echte Abfrage – deine Formulare auflisten:

    curl https://propform.io/api/v1/forms \
      -H "Authorization: Bearer pf_live_DEIN_KEY"
    

Authentifizierung

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.

  • Keys verwaltest du unter Einstellungen → API-Keys: erstellen, benennen und jederzeit widerrufen.
  • Ein widerrufener Key verliert sofort den Zugriff.
  • Pro Account sind maximal 10 API-Keys möglich.
  • Behandle den Key wie ein Passwort – er hat vollen Zugriff auf deine Formulare und Einsendungen. Gib ihn nicht im Browser-Frontend oder in öffentlichen Repositories preis.

Rate-Limits

  • 60 Anfragen pro Minute und Key.
  • Zusätzlich 300 Anfragen pro Minute und IP-Adresse (Schutz vor Überlast).

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.

Antwortformat, Paginierung und Fehler

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

Formulare

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
POST /forms/{id}/copy Formular kopieren (inkl. Felder, Regeln, E-Mail-Konfiguration); die Kopie startet 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:

  • PATCH-Semantik: Felder, die du nicht mitschickst, bleiben unverändert. Du kannst also gefahrlos einzelne Einstellungen ändern.
  • Aktivieren ("active": true) funktioniert nur, solange dein Tarif-Limit aktiver Formulare nicht erreicht ist (sonst 422). Deaktivieren geht immer.
  • Formulare können per API nicht gelöscht werden – das geht bewusst nur im Dashboard.
  • Nicht schreibbar sind u.a. slug, Zähler und Vorlagen-Flags. Passwörter (z.B. Formular-Passwort) sind schreibbar, tauchen aber nie in Antworten auf.
  • Die Liste enthält alle deine Formulare – auch solche, die als Kopiervorlage freigegeben sind. Das Feld form_template (nur lesbar) zeigt die Freigabe an; mit dem Filter template=0/1 kannst du danach filtern.
  • Jede Formular-Antwort enthält die öffentliche url des Formulars.

Häufig genutzte Formular-Einstellungen

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.

Felder

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}'

Häufig genutzte Feld-Eigenschaften

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)

Darstellungsvarianten per API

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: 13 (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_mode bleibt 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 wir display_variant: "slider" — hat ein Feld beides gesetzt, gewinnt display_variant.

Verknüpfungs-Feld per API

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]}'

onOffice-Feldkonfiguration

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 }
}

Bedingungen (Regeln)

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"}]
  }]}'
  • Operatoren: equals, not_equals, empty, not_empty, starts_with, not_starts_with, contains, not_contains
  • Aktionen: show, 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
  • Als Ziel sind neben Feld-IDs auch virtuelle Ziele erlaubt: __form_title, __form_description, __submit, __submit_spinner, __multistep_back, __multistep_next (dort nur set_label, show, hide; beim Spinner nur set_label).
  • Alle referenzierten Feld-IDs müssen zum Formular gehören.
  • Grenzen pro Anfrage: max. 200 Regeln, je 50 Klauseln/Aktionen.
  • Bedingungen stehen wie im Dashboard erst ab dem Tarif mit 10 aktiven Formularen zur Verfügung (sonst 403).

Details zur Funktionsweise der Regeln findest du unter Bedingungen, Regeln & Berechnungen.

Einsendungen und Statistiken

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.

Webhooks (neue Einsendungen in Echtzeit)

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-Signaturesha256=<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" } }
}

Freitext / Nachricht mit onOffice-Makros (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:

  • Es funktionieren dieselben Makros wie in den E-Mail-Templates von propform (onOffice-Feldmakros, _getAddressLink, _getEstateLink, …).
  • Ein Makro kann nur aufgelöst werden, wenn die Einsendung einen passenden Datensatz-Kontext hat (z.B. braucht _objekttitel eine verknüpfte Immobilie). Ohne Kontext bleibt das Makro unaufgelöst im Text stehen.
  • Werte, die erst nach dem Webhook-Versand geschrieben werden (Datei-Uploads zu onOffice, per „erweitern" beschriebene Multiselect-Felder), können im aufgelösten Text noch fehlen.
  • Ohne Freitext-Template wird kein Nachrichtenfeld gesendet.
  • Maximal 5.000 Zeichen.

Signatur prüfen (empfohlen)

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);
});

Zustellung, Wiederholung und Grenzen

  • Dein Endpunkt sollte mit einem 2xx-Status antworten. Bei Fehlern versucht propform die Zustellung bis zu 3-mal (mit Wartezeit).
  • Schlägt ein Abo 20-mal in Folge fehl, wird es automatisch deaktiviert (is_active: false, sichtbar über GET /webhooks). Lege es danach einfach neu an.
  • Pro Account sind maximal 20 Webhook-Abos möglich.
  • Dateianhänge sind nicht Teil der Payload.

Formular-Ordner

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.

Formular-Gruppen

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.

PDF-Vorlagen (PDF-Generator)

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:

  • Die Antwort ist immer der normalisierte Zustand nach dem Speichern – exakt das, was auch der Vorlagen-Editor anzeigen würde. Unbekannte Schlüssel und ungültige Feld-Referenzen werden dabei still verworfen: Vergleiche im Zweifel die Antwort mit dem, was du gesendet hast.
  • Die Blockliste ist ein Schnappschuss: Felder, die du nach dem Anlegen der Vorlage per API hinzufügst, landen nicht automatisch darin. Nutze regenerate (ersetzt eigene Block-Anpassungen!) oder ergänze die Blöcke gezielt per PATCH.
  • Schaltest du settings.document_signing.enabled ein, sorgt propform automatisch für einen Dokument-Unterschriftsblock in der Vorlage (falls keiner vorhanden ist).

Blockmodell-Referenz

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.

Briefpapiere

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"
  • Verschlüsselte oder beschädigte PDFs werden mit 422 abgelehnt. Bilder mit niedriger Auflösung werden angenommen, die Antwort enthält dann aber einen Hinweis im Feld warning.
  • Zuweisen ans Formular: design.letterhead_id im PDF-Vorlagen-PATCH setzen (Konto-Briefpapiere überall, formprivate nur im eigenen Formular).

Konto-Einstellungen

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_13) + 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.
  • 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.
  • E-Mail-Adresse (Login), Passwort, API-Keys, Zertifikate und Abo-/Zahlungsdaten sind kein Teil der API.
  • Die E-Mail-Slots in notifications werden als kompakte Liste gespeichert: Lücken rücken nach dem Speichern auf.

onOffice-Konfiguration

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)

Mitarbeiter (Mitarbeiterbereich)

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
  • Das Platz-Limit deines Tarifs (staff_area.seat_limit unter /account) wird beim Anlegen, Aktivieren und Importieren hart durchgesetzt (422). Nur aktive Mitarbeiter belegen einen Platz.
  • Bei onOffice-Mitarbeitern kommen Name, E-Mail und Gruppen aus dem täglichen onOffice-Abgleich – die E-Mail ist deshalb per API nicht änderbar.
  • Deaktivieren (is_active: false) beendet sofort alle laufenden Sessions des Mitarbeiters.
  • Löschst du eine Gruppe, die in Formular-Freigaben verwendet wird, wird sie dort entfernt; bleibt keine Freigabe übrig, ist das Formular für niemanden freigegeben (bewusst restriktiv) – setze die Freigabe dann neu.
  • Welche Formulare geschützt sind und wer sie sehen darf, steuerst du über die Formular-Felder 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.

Tipps für Zapier, Make & Co.

  • Trigger „Neue Einsendung": Webhook-Abo anlegen (siehe oben) – zuverlässiger und schneller als regelmäßiges Abfragen.
  • Polling-Alternative: GET /forms/{id}/submissions?sort=desc und die höchste bereits verarbeitete id merken.
  • Verbindungstest: GET /me.
  • Neue Formulare erkennen: GET /forms?updated_since=….
  • Bei 429 ein kurzes Warten einbauen und den Retry-After-Header respektieren.

Versionierung

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.