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).
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.
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" } }
First real query – list your forms:
curl https://propform.io/api/v1/forms \
-H "Authorization: Bearer pf_live_DEIN_KEY"
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.
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.
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 |
| 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:
"active": true) only works as long as your plan’s limit on active forms has not been reached (otherwise 422). Deactivation is always possible.slug, counters and template flags. Passwords (e.g. form password) are writable, but never appear in responses.url.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.
| 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}'
| 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]}'
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.
| 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"}]
}]}'
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 (only set_label, show, hide are permitted there; for the spinner, only set_label).403).You can find details on how the rules work under Conditions, Rules & Calculations.
| 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.
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-Signature – sha256=<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" } }
}
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:
_getAddressLink, _getEstateLink, …)._objekttitel requires a linked property). Without context, the macro remains unresolved in the text.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);
});
2xx status. In the event of errors, propform will attempt delivery up to 3 times (with a waiting period).is_active: false, visible via GET /webhooks). Simply create it again afterwards.GET /form-groups lists your form groups with their IDs. You can assign the IDs in the form PATCH as form_groups (array).
GET /forms/{id}/submissions?sort=desc and the highest id already processed.GET /me.GET /forms?updated_since=….429, introduce a short delay and respect the Retry-After header.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).