propform API (REST)

With the propform API, you can manage your forms, submissions and statistics from external applications – for example, from your own tools, scripts or automation platforms such as Zapier, Make or n8n.

> Base URL: https://propform.io/api/v1 > > All requests and responses are in JSON (Content-Type: application/json). Timestamps are in ISO 8601 format (e.g. 2026-07-07T08:30:00+00:00).

Quick start in 3 steps

  1. Create an API key: Go to Settings → API Keys, click on Create API Key and copy the key displayed (format pf_live_…). For security reasons, it will be displayed to you only this once.

  2. Test the connection: Replace pf_live_DEIN_KEY and call:

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

    Response:

    { "data": { "id": 42, "name": "Max Makler", "email": "max@makler.de" } }
    
  3. First real query – list your forms:

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

Authentication

The API uses API keys as bearer tokens. Every request requires the key in the Authorization header (Authorization: Bearer pf_live_…). If no key is provided or the key is invalid, the API responds with 401.

  • You can manage keys under Settings → API Keys: create, name and revoke them at any time.
  • A revoked key loses access immediately.
  • A maximum of 10 API keys are permitted per account.
  • Treat the key like a password – it has full access to your forms and submissions. Do not disclose it in the browser frontend or in public repositories.

Rate Limits

  • 60 requests per minute per key.
  • Additionally, 300 requests per minute per IP address (to prevent overload).

If these limits are exceeded, the API responds with 429 Too Many Requests and an Retry-After header (seconds until the next permitted attempt). Include a short delay in your automations if you see this status.

Response format, pagination and errors

Successful responses return the data in the data field.

Lists are paginated. Control them via per_page (default 25, max. 100) and page. The response also contains links and 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 }
}

Errors always have an message field; for validation errors (422), the details for each field are contained in errors:

{
  "message": "The url field is required.",
  "errors": { "url": ["The url field is required."] }
}
Status Meaning
200 / 201 Success (201 = newly created)
204 Success, no response data (e.g. deletion)
401 No API key or invalid API key
403 The record does not belong to your account (or feature not included in your plan)
404 Record does not exist
409 Action currently not possible (e.g. not connected to onOffice)
422 Validation error – see field errors for details
429 Rate limit reached – see Retry-After

Forms

Method Endpoint Description
GET /forms List your own forms. Filter: active (0/1), updated_since (date), per_page, page
POST /forms Create a new form – only internal_title required; starts in an inactive state
GET /forms/{id} Read the entire form (including fields, email and ChatGPT configuration)
PATCH /forms/{id} Configure form – only the submitted fields are changed
POST /forms/{id}/copy Copy form (including fields, rules and email configuration); the copy starts in an inactive state

The form is a resource in the API: you can also read and write the settings for email sending and ChatGPT integration directly on the form. The field names correspond exactly to the settings in the form 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}'

Important points to note:

  • PATCH semantics: Fields that you do not include in the request remain unchanged. This means you can safely modify individual settings.
  • Activate ("active": true) only works as long as your plan’s limit on active forms has not been reached (otherwise 422). Deactivation is always possible.
  • Forms cannot be deleted via the API – this is deliberately restricted to the dashboard.
  • Items that are not writable include slug, counters and template flags. Passwords (e.g. form password) are writable, but never appear in responses.
  • Every form response contains the form’s public url.

Frequently used form settings

A form has over 100 settings (all of which are also available in the editor). The most important ones are:

Field Meaning
internal_title Internal name (visible only in the dashboard)
title Public title of the form
custom_slug Descriptive URL part (must be unique within your account)
active Form online (true) or offline (false)
description Introductory text above the form
submit_button_label Text on the submit button
thankyou_headline / thankyou_text Heading and text of the thank-you page
redirect Redirect URL after submission (instead of the thank-you page)
background_color / accent_color Design colours (hex, e.g. #0d6efd)
subject / body / receiver Subject, text and recipient of the confirmation email
send_email_via_onoffice Send confirmation email via onOffice

You can view the complete object at any time via GET /forms/{id}.

👉 You'll find the complete list of all fields and formats in the API field reference.

Fields

Method Endpoint Description
GET /forms/{id}/fields Fields in a form (sorted by position)
POST /forms/{id}/fields Create field – onoffice_module is mandatory, position automatically set at the end
GET /fields/{id} Read a single field
PATCH /fields/{id} Change field (sent properties only)
DELETE /fields/{id} Delete field
PATCH /forms/{id}/fields/reorder Reorder fields – field_ids must contain all field IDs of the form in the desired order
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}'

Frequently used field properties

Field Meaning
onoffice_module Target module: address, estate, task, calendar, agentsLog (mandatory when creating)
onoffice_field_name Target field name in onOffice (see onOffice field configuration)
label Field label in the form
hint / placeholder Help text or placeholder in the input field
required Required field (true/false)
hidden / disabled / read_only Hidden / disabled / read-only
default_value Default value
half_size Display field as half-width on large screens
datalist Selection values as a semicolon-separated list (Rot;Grün;Blau)

Sorting: For reorder, the order in the field_ids array determines the new position. The array must contain exactly all field IDs of the form (if one is missing or an unrelated one is included → 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 field configuration

GET /onoffice/fields returns the field configuration for your onOffice account – per module (address, estate, task, calendar, agentsLog, file, project) – listing all fields with their label, type, length and permitted values. This is how you find the valid values for onoffice_field_name. Use ?module=address to filter by module.

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

> A working onOffice API connection is required; otherwise, the endpoint will return 409.

Conditions (Rules)

Method Endpoint Description
GET /forms/{id}/rules Rules of a form (sorted by priority)
PUT /forms/{id}/rules Replace all rules of the form (empty array = delete all)

The write endpoint works in the same way as the condition editor in the dashboard: All rules are always saved in one go (no individual additions). So first read the list using GET, modify it, and then send the entire list back using PUT. A rule consists of clauses (if conditions, linked to mode = all/any) and actions (Then actions):

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"}]
  }]}'
  • Operators: equals, not_equals, empty, not_empty, starts_with, not_starts_with, contains, not_contains
  • Campaigns: 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
  • In addition to field IDs, virtual targets are also permitted: __form_title, __form_description, __submit, __submit_spinner, __multistep_back, __multistep_next (only set_label, show, hide are permitted there; for the spinner, only set_label).
  • All referenced field IDs must belong to the form.
  • Limits per request: max. 200 rules, 50 clauses/actions each.
  • As with the dashboard, conditions are only available from the plan with 10 active forms onwards (otherwise 403).

You can find details on how the rules work under Conditions, Rules & Calculations.

Submissions and statistics

Method Endpoint Description
GET /forms/{id}/submissions Submissions of a form. Filters: created_after, created_before, sort (asc/desc), per_page
GET /submissions/{id} Single submission
GET /forms/{id}/stats Page views, submissions and conversion rate; optional from/to (date)

The content of a submission (content) is grouped by onOffice module:

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

If you have disabled the saving of submission data in the privacy settings, content is always null – but the submission still counts towards the statistics. The stats response contains views, submissions and conversion_rate.

Webhooks (new submissions in real time)

Instead of regularly checking for submissions, you can be notified via a webhook: propform immediately sends an POST to your URL whenever a new submission is made.

💡 You can also manage webhooks in the frontend without an API key: in the settings under "Webhooks" (for all forms) or directly in the form editor under "Webhooks (Zapier, n8n, Make …)".

Method Endpoint Description
GET /webhooks List your webhook subscriptions
POST /webhooks Create a subscription: url (required), form_id (optional – if not specified, the subscription applies to all your forms), payload_template (optional – see below), message_key (optional – field name of the resolved message, default text)
DELETE /webhooks/{id} Delete subscription

URL requirements: It must be publicly accessible and use http:// or https://. Internal or local addresses (e.g. localhost, 127.0.0.1, private network IPs) are rejected for security reasons (422). As the submission data is transmitted, please use https:// where possible.

The response when creating the delivery contains secret, which is used to sign each delivery (you can retrieve it later at any time using GET /webhooks):

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

Each delivery contains two headers:

  • X-Propform-Event – the event type (currently always submission.created)
  • X-Propform-Signaturesha256=<HMAC-SHA256 des Request-Bodys mit deinem secret>

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

Free text / message with onOffice macros (payload_template)

Optionally, you can store a free-text template for each subscription. After every form submission, propform resolves onOffice macros in it (against the records the submission created or edited) and includes the result as a dedicated message field in the payload — ideal as a ready-made message for Slack & co., without having to assemble anything in your receiving workflow. The field is named text by default (see below).

Example template:

New inquiry from _Vorname _Name about _objekttitel → _getEstateLink

becomes the following in the payload:

"text": "New inquiry from Max Mustermann about Traumwohnung am Stadtpark → https://smart.onoffice.de/…"

Message field name (message_key): By default the field is named text – this way Slack, Microsoft Teams and Google Chat interpret the payload directly as a message. Discord expects content. For Zapier/n8n/Make the name is arbitrary. You can change it per webhook via the message_key parameter (in the frontend via "Message field name").

Please note:

  • The same macros work as in propform's email templates (onOffice field macros, _getAddressLink, _getEstateLink, …).
  • A macro can only be resolved if the submission has a matching record context (e.g. _objekttitel requires a linked property). Without context, the macro remains unresolved in the text.
  • Values that are only written after the webhook is sent (file uploads to onOffice, multiselect fields written via "extend") may still be missing from the resolved text.
  • Without a free-text template, no message field is sent.
  • Maximum 5,000 characters.

Verify the signature (recommended)

Verify the signature to ensure that the request really originates from propform. Calculate the HMAC using the raw request body (not the parsed JSON) and compare it in real time.

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

Delivery, retries and limits

  • Your endpoint should respond with an 2xx status. In the event of errors, propform will attempt delivery up to 3 times (with a waiting period).
  • If a subscription fails 20 times in a row, it is automatically deactivated (is_active: false, visible via GET /webhooks). Simply create it again afterwards.
  • A maximum of 20 webhook subscriptions are permitted per account.
  • File attachments are not part of the payload.

Form groups

GET /form-groups lists your form groups with their IDs. You can assign the IDs in the form PATCH as form_groups (array).

Tips for Zapier, Make & Co.

  • ‘New submission’ trigger: Create a webhook subscription (see above) – more reliable and faster than regular polling.
  • Polling alternative: Note GET /forms/{id}/submissions?sort=desc and the highest id already processed.
  • Connection test: GET /me.
  • Detect new forms: GET /forms?updated_since=….
  • For 429, introduce a short delay and respect the Retry-After header.

Versioning

The current version is v1 and is fixed in the path (/api/v1). We add new fields and endpoints in a backwards-compatible manner without changing the version – your client should therefore simply ignore any unknown fields in responses. Fundamental, non-backwards-compatible changes would be released under a new version (/api/v2).