Mit der propform-API kannst du deine Formulare, Einsendungen und Statistiken 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.
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), updated_since (Datum), 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:
"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.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 (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) |
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.
| 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.GET /form-groups listet deine Formular-Gruppen mit IDs auf. Die IDs kannst du im Formular-PATCH als form_groups (Array) zuweisen.
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.