propform-API (REST)

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/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), 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:

  • 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.
  • 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 (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]}'

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.

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

GET /form-groups listet deine Formular-Gruppen mit IDs auf. Die IDs kannst du im Formular-PATCH als form_groups (Array) zuweisen.

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.