With the propform API, you can manage your forms, submissions, statistics, PDF templates, letterheads, staff members and account settings 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.
403 with error.code api_key_area_missing (error.area names the area, the message the way to grant it). If the onOffice data area is not activated for your account: 403 with onoffice_data_not_enabled.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), template (0/1), updated_since (date), folder (folder ID or none), 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; form_template: true (copy release: any propform account that knows the link can copy the form) only together with confirm: true |
POST |
/forms/{id}/copy |
Copy form (including fields, rules, email configuration and the PDF template; a letterhead private to the source form is not carried over); the copy starts in an inactive state |
POST |
/forms/copy-template |
Copy a released template of another account into your own: body slug (the 12 characters of its public link); 403 if the owner has not released it; PDF design and letterhead of your account, onOffice field bindings unchecked, copy inactive |
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.form_template indicates that sharing (writable via PATCH, true only with confirm: true); use the template=0/1 filter to filter by it.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 β or relationlist for the Link field (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) |
display_variant |
Display variant of the field (see below) |
select_option_meta |
Additional data per selection key for card/icon/image displays (JSON string, see below) |
With display_variant you control how a field is displayed in the form β the stored value and the onOffice connection remain identical. null or empty = standard display. Valid values per field type:
| onOffice field type | Values |
|---|---|
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 |
From/to search criterion (integer/float in the searchcriteria module) |
rangeslider |
Unknown values or values that don't match the field type are ignored when rendering (standard display). For cards, icons and images, select_option_meta provides the additional data per onOffice key value β a JSON string of the form:
{"haus": {"icon": "home", "description": "Detached or terraced house"}, "wohnung": {"image": "https://β¦/wohnung.jpg"}}
icon is an icon name from the integrated Tabler Icons set (lowercase letters/digits/hyphens only, e.g. home, building-estate, currency-euro), image an http(s) image URL (ideally from the media library), description a text below the option title (cards, icon and image tiles). Icons appear on icon tiles, in front of the option label in the button group and above the buttons of the Likert scale. Empty or unknown entries are ignored. For the slider, minimum/maximum value and step come from min_value/max_value/step (also for the from/to rangeslider); for the rating, max_value determines the number of steps (2β10, default 5).
Settings per display variant (all optional; unsuitable values are ignored when rendering):
| Property | Applies to | Meaning |
|---|---|---|
variant_axis_labels |
slider |
Custom labels below the slider, semicolon-separated (e.g. cheap;medium;expensive); takes precedence over datalist |
slider_allow_input |
slider, rangeslider |
true = value can additionally be typed into a number field |
range_default_von / range_default_bis |
rangeslider + standard from/to |
Prefills From and To separately (takes precedence over default_value, which fills both sides equally); on the slider, presets β unlike untouched handles β are sent |
scale_label_left / scale_label_right |
stars, nps, likert |
Labels for the scale endpoints |
rating_symbol |
stars |
stars (default), hearts, smileys (mood scale, 2β5 steps; above that, falls back to stars) or custom (custom icon via rating_custom_icon) |
rating_custom_icon |
stars |
Tabler icon name for rating_symbol: "custom" (invalid/empty β stars) |
variant_tile_size |
cards, icons, images |
compact or large (empty = normal) |
variant_image_ratio |
images |
square or 16x9 (empty = 4:3) |
variant_image_fit |
images |
contain = fit the whole image (empty = fill/crop) |
multiselect_min_selected / multiselect_max_selected |
multi-select displays | Minimum/maximum number of selectable options (enforced in the browser) |
calendar_date_mode |
calendar |
future (from today only) or past (until today only) |
calendar_min_date / calendar_max_date |
calendar |
Fixed earliest/latest date (YYYY-MM-DD); takes precedence over calendar_date_mode |
calendar_disable_weekends |
calendar |
true = block weekends |
yesno_yes_label / yesno_no_label |
yesno |
Custom button labels (empty = "Yes"/"No" in the account language) |
yesno_icons |
yesno |
true = icons on the buttons (default check/cross) |
yesno_yes_icon / yesno_no_icon |
yesno |
Custom Tabler icons instead of check/cross (only take effect with yesno_icons: true) |
variant_columns |
radio, checkboxes |
List columns: 1β3 (always single-column on mobile) |
variant_centered |
buttons/tiles/chips/yesno/stars/nps/calendar/sliders |
true = centered instead of left-aligned (for sliders, the type-in field) |
variant_icon_color |
icons, yesno, buttons, likert |
Custom icon color as hex (#RRGGBB); empty = inherits the text color |
Multi-select note: The tile/list displays show the options flat; there is no parent/child automation there. The stored
multiselect_modeis left unchanged when you set a display variant and takes effect again as soon as you switch back to the default display.
Slider note: The simple slider β like the from/to slider β only sends a value after an interaction; an untouched slider behaves like an empty field.
Note: The older range slider via
special_field: "range"keeps working unchanged (legacy). For new sliders we recommenddisplay_variant: "slider"β if a field has both set,display_variantwins.
You create the Link field (display, link, create, edit and unlink connected onOffice records) with onoffice_module: "relationlist". The relation type consists of relation_type (e.g. estate:address:owner) plus relation_anchor_module (estate or address β which side is the loaded form record); on top of that come the switches relation_show_existing, relation_allow_search, relation_allow_create, relation_allow_edit, relation_allow_remove, the duplicate check (relation_duplicate_check + relation_duplicate_check_fields) and the mini-form element list relation_subform_elements (array of {type: "field", name, required, half} or layout elements {type: "headline"|"description"|"dividingline"|"collapsible", text}). For creation there are also relation_advisor_mode (anchor = use the agent of the loaded record, fixed = fixed user from relation_advisor_user_id) and relation_auto_open_create (open the mini form automatically when no connections are displayed). All values and limits are listed in the field reference.
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": "Owner",
"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}
]
}'
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.
POST /onoffice/fields/refresh reloads the field configuration from onOffice β for example after you have created a field or changed permitted values in onOffice. The refresh runs in the background: the response is 202 with data.status = pending; a few seconds later GET /onoffice/fields returns the new state. Both endpoints return a meta object with the time of the last build (updated_at), the language of the field labels (locale) and refresh_pending (true while a refresh triggered via the API is still running). Without this call, propform refreshes the configuration once a day and every time the form overview is opened. Limit: 120 calls per hour.
{
"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 }
}
| 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.Folders structure the forms overview in the dashboard. Each form sits in at most one folder (form_folder_id, null = "No folder"). Folders can have one sublevel (parent_id, at most two levels). Not to be confused with the form groups, which control the copy sharing.
| Method | Endpoint | Description |
|---|---|---|
GET |
/form-folders |
Your folders with IDs and form count |
POST |
/form-folders |
Create a folder β name required (max. 100 characters), optional parent_id (ID of a top-level folder β creates a subfolder) |
PATCH |
/form-folders/{id} |
Rename a folder (name) and/or move it (parent_id: ID of a top-level folder or null = top-level) |
DELETE |
/form-folders/{id} |
Delete a folder β the forms are kept and appear under "No folder" again, subfolders become top-level folders |
Assign a form via the form PATCH using form_folder_id (ID of one of your folders, or null). Filter the forms list with ?folder=<id> or ?folder=none.
| Method | Endpoint | Description |
|---|---|---|
GET |
/form-groups |
Your groups with their IDs, copy flag and form count |
POST |
/form-groups |
Create a group β only name required |
PATCH |
/form-groups/{id} |
Change name and/or copy (whether the forms in this group can be copied) |
DELETE |
/form-groups/{id} |
Delete a group β the forms remain and only lose their group assignment |
You assign the group IDs in the form PATCH as form_groups (array).
propform can generate its own PDF from every submission (see PDF generator). You can read and write a form's template β block list, design and settings β entirely via the API:
| Method | Endpoint | Description |
|---|---|---|
GET |
/forms/{id}/pdf-template |
Read the template (enabled, name, blocks, design, settings); if none exists yet, one is automatically suggested from the form fields |
PATCH |
/forms/{id}/pdf-template |
Partial update β only the submitted parts are changed |
POST |
/forms/{id}/pdf-template/regenerate |
Regenerate the block list from the current set of fields (design, settings and active status are kept) |
GET |
/forms/{id}/pdf-template/preview |
Real PDF preview with sample data (application/pdf; max. 10 requests/minute) |
GET |
/account/pdf-design |
Read the account-wide design preset (design + settings) |
PATCH |
/account/pdf-design |
Change the design preset β new form templates inherit it as their starting point |
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"}}'
Important points to note:
regenerate (which replaces your own block customisations!) or add the blocks specifically via PATCH.settings.document_signing.enabled, propform automatically ensures that the template contains a document signature block (if there is none yet).blocks is an array of blocks. Each block has a type, an automatically assigned id and type-specific properties (max. 300 blocks per template):
| Type | Properties |
|---|---|
heading |
text (max. 500 characters), level (1, 2 or 3) |
paragraph |
text (max. 5,000 characters) |
field |
Single field with its value: field_id (field of this form) or onoffice (reference, see below), optional label |
fieldlist |
Label-value list: field_ids = array of field IDs and/or onOffice references |
columns |
Two columns: left and right are each block arrays (one level deep) |
image |
url (http/https), width_percent (10β100), align (left/center/right), caption |
divider |
style: line (line) or space (spacing) |
pagebreak |
Page break, no properties |
signature |
field_id = signature field of the form or null = document signature after submitting; label; required (document signature only) |
onOffice references: In field/fieldlist, instead of a field ID you can also output a field of the linked onOffice record: {"module": "address", "name": "Telefon1", "label": "Phone"} (modules address and estate; the field must exist in your onOffice field configuration).
Example:
{
"blocks": [
{"type": "heading", "text": "Tenant self-disclosure", "level": 1},
{"type": "paragraph", "text": "Details provided by the prospect:"},
{"type": "fieldlist", "field_ids": [4711, 4712, {"module": "estate", "name": "objekttitel", "label": "Property"}]},
{"type": "pagebreak"},
{"type": "signature", "field_id": null, "label": "Place, date, signature", "required": true}
]
}
Design keys (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 (see Letterheads) as well as letterhead_margin_top/right/bottom/left (0β100 mm, null = margins of the letterhead).
Settings keys (settings): empty_fields (hide/dash/blank), embed_uploaded_images, attach_uploaded_pdfs, embed_existing_files, embedded_image_size (small/column/full), document_signing.enabled.
Your own letterhead (PDF or image) is placed as a background under every page of the generated PDF β optionally with its own continuation sheet from page 2 onwards:
| Method | Endpoint | Description |
|---|---|---|
GET |
/letterheads |
Your letterheads; with ?form_id= exactly those selectable in this form |
POST |
/letterheads |
Upload (multipart/form-data, see below) |
PATCH |
/letterheads/{id} |
Change letterhead_name and margins |
DELETE |
/letterheads/{id} |
Delete β templates that use it will render in the standard layout again afterwards |
Upload parameters (POST, multipart): letterhead_name (required), letterhead_first (required, PDF/PNG/JPG up to 10 MB β the first sheet), letterhead_continuation (optional β the continuation sheet, same file types), letterhead_margin_top/right/bottom/left (optional, mm), form_id (optional β this makes the letterhead form-private; without it, it goes into the account library).
curl -X POST https://propform.io/api/v1/letterheads \
-H "Authorization: Bearer pf_live_DEIN_KEY" \
-F "letterhead_name=Company letterhead" \
-F "letterhead_first=@letterhead.pdf" \
-F "letterhead_continuation=@continuation-sheet.pdf"
422. Low-resolution images are accepted, but the response then contains a note in the warning field.design.letterhead_id in the PDF template PATCH (account letterheads can be used everywhere, form-private ones only in their own form).| Method | Endpoint | Description |
|---|---|---|
GET |
/account |
Read account settings, grouped |
PATCH |
/account |
Partial update β only the submitted groups/fields are changed |
The response is grouped by the settings areas of the dashboard:
| Group | Content | Writable |
|---|---|---|
profile |
name (profile/company name) |
β |
styling |
All default_* design defaults for new forms (colours, fonts, logo, card look β¦) |
β |
imprint |
Default imprint: default_privacypolicy_url, default_imprint_url, default_homepage_url, default_imprint_line_1 to _5 |
β |
notifications |
Up to 3 recipients of technical notifications (notification_email_1β3) + notification_self |
β |
statistics |
save_submission_data, save_ips (privacy) |
β |
domain |
username (subdomain), externaldomain, externaldomain_active |
β read-only |
staff_area |
enabled, session_days (1β90), seat_limit + active_members (read-only) |
partially |
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 is deliberately read-only: Changing the subdomain or domain would break all form links that are already embedded β this is only possible in the dashboard.default_google_font_name, default_headline_font): Only the names listed as choices for google_font_name / headline_font in the field reference are accepted β any other name is rejected with 422.staff_area.enabled can only be activated if your plan includes staff seats (seat_limit > 0, otherwise 422). seat_limit itself is determined by your plan and is not writable.notifications are stored as a compact list: gaps are closed up after saving.| Method | Endpoint | Description |
|---|---|---|
GET |
/account/onoffice |
Connection status and settings of the onOffice integration |
PATCH |
/account/onoffice |
Change settings (max. 5 requests/minute) |
The response contains, among other things, api_type (normal/marketplace_api), connection_ok, credentials_set, email_onoffice_api_user (+ _is_functional), field_config_locale and reason_cancellation. For security reasons, the API token and secret never appear in any response.
Changeable via PATCH:
| Field | Meaning |
|---|---|
api_type |
normal (your own onOffice API credentials) or marketplace_api (Marketplace integration; requires a connected Marketplace user) |
email_onoffice_api_user |
Email identities of the API user as an array (max. 10) β each address is verified via a test email |
email_onoffice_marketplace_api_user |
ditto for the Marketplace integration |
field_config_locale |
Language of the onOffice field labels (de, en, β¦) β the field configuration cache is then rebuilt in the background |
reason_cancellation |
Your own cancellation reasons as an array (max. 50 entries, 40 characters each; empty = default list) |
Forms with staff access protection enabled can only be reached after logging in via magic link. You manage the staff members and groups behind it via the API:
| Method | Endpoint | Description |
|---|---|---|
GET |
/staff-members |
Staff members including group assignment, source (onoffice/manual) and is_active |
POST |
/staff-members |
Create an external staff member: name, email, optional groups (array of group IDs) |
PATCH |
/staff-members/{id} |
Partial update: name, email (manual members only), groups, is_active |
DELETE |
/staff-members/{id} |
Delete staff member (also ends any running sessions) |
POST |
/staff-members/import-onoffice |
Import onOffice users: onoffice_user_ids (array, optional β if not specified, all importable ones). Response: created, updated |
GET |
/staff-groups |
Groups including member count (onoffice_group_id set = mirrored from onOffice) |
POST |
/staff-groups |
Create a local group (name) |
DELETE |
/staff-groups/{id} |
Delete group |
staff_area.seat_limit under /account) is strictly enforced when creating, activating and importing (422). Only active staff members occupy a seat.is_active: false) immediately ends all of the staff member's running sessions.staff_only_enabled, staff_allowed_member_ids and staff_allowed_group_ids in the form PATCH; the account switches (staff_area.enabled, session_days) are under /account.You read and maintain the search profiles (search criteria) of your onOffice contacts via your account's official onOffice API. Stored onOffice API credentials are required (otherwise 409). In addition, propform must have activated the onOffice data tools for your account and the API key must carry the onOffice data area (otherwise 403, see Authentication).
| Method | Endpoint | Description |
|---|---|---|
GET |
/onoffice/search-profiles/fields |
Active search criteria (set "Kein Template") with type (kind) and selection keys; ?fresh=1 reads live instead of from the field cache |
GET |
/onoffice/addresses/{address}/search-profiles |
Search profiles of an address (internal address id); limit (max. 100), offset |
GET |
/onoffice/search-profiles?customer_number=β¦ |
The same by customer number |
GET |
/onoffice/search-profiles/{id} |
One search profile in full |
POST |
/onoffice/addresses/{address}/search-profiles |
Create a search profile: values, radius, note, public_note, advisor, expiry_date, ko, active (default true) β response 201 |
PATCH |
/onoffice/search-profiles/{id} |
Change only what is sent; only active = activate/deactivate |
DELETE |
/onoffice/search-profiles/{id} |
Delete for good β only with confirm=true |
POST |
/onoffice/search-profiles/match |
Matching search profiles: searchdata (field β value, as for a property) and/or estate_id; limit (max. 50), offset, count_only, group_by_address (default true) |
POST /api/v1/onoffice/addresses/3415/search-profiles
{
"values": {"vermarktungsart": "kauf", "objektart": "haus", "kaufpreis": {"from": 200000, "to": 450000}},
"radius": {"city": "LΓΌbeck", "km": 30},
"public_note": "Looking for a house with a garden"
}
{"from", "to"} (one side may be missing, 0 = no bound), selection fields by key, objekttyp and regionaler_zusatz as a list; the radius goes under radius (km, zip, city, street, house_number, country).422 without changing anything in onOffice.ko (list of field names of active search criteria, plus Umkreis) replaces the profile's KO criteria, [] clears them; unknown names β 422. Responses list them under ko.{from, to} (an unset side is null), the radius under radius and the public note as public_note. The onOffice API does not return the internal note (note) or the expiry date (expiry_date) β both are write-only (write_only lists them), and note replaces an existing note unseen.502 with message and error.onoffice_errorcode; unknown profile, address or property β 404.502 with onoffice_errorcode 156; for Marketplace connections, propform support can help). Without advisor, onOffice enters the API user as advisor.You read, upload, change and delete files of your estates and addresses via your account's official onOffice API. {module} is estates or addresses; a file id is only valid together with its record. Requires stored onOffice API credentials (otherwise 409). In addition, propform must have activated the onOffice data tools for your account and the API key must carry the onOffice data area (otherwise 403, see Authentication).
| Method | Endpoint | Description |
|---|---|---|
GET |
/onoffice/files/meta |
File types (type) and document attributes of your account; what works per module |
GET |
/onoffice/{module}/{record}/files |
Files of a record; limit (max. 100), offset. For estates with title, type, document attribute, position and publication |
GET |
/onoffice/{module}/{record}/files/{file} |
Metadata of a file |
GET |
/onoffice/{module}/{record}/files/{file}/content |
Content as a file (binary, up to 25 MB; header X-File-Sha256) |
POST |
/onoffice/{module}/{record}/files |
Upload β exactly one source: multipart field file (up to 128 MB), content_base64 + filename (up to 20 MB) or source_url (public https address, up to 20 MB). For estates also title, type, free_text, document_attribute, watermark, position (0 = first place), publish (only with confirm=true) β response 201 |
POST |
/onoffice/estates/{record}/files/link |
Link file: url, type (Link, Ogulo-Link, Film-Link, Objekt-Link), title, free_text β estates only |
PATCH |
/onoffice/estates/{record}/files/{file} |
Change only what is sent: title, type, free_text, document_attribute ("" removes it; "" clears title/free_text) β estates only |
DELETE |
/onoffice/{module}/{record}/files/{file} |
Delete for good β only with confirm=true |
curl -X POST https://propform.io/api/v1/onoffice/estates/3813/files \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@floor-plan.png" -F "type=Grundriss" -F "title=Floor plan ground floor"
title, type, free_text and document_attribute with 422, likewise changing and link files. Uploading, reading and deleting work.publish=true (with confirm=true) applies your account's default release rights. onOffice still lists photos and title images as published on the homepage (onOffice's default for images) β the response shows the actual state (published_on_homepage, publication_status). Publication and position cannot be changed afterwards via the API.document_attribute_moved_from). A new title image turns the previous one into a photo β without confirmation, the response names it (previous_title_image); if the estate has none, onOffice itself makes the first uploaded image the title image β the response says so in a warning (the image is then visible on the homepage). To keep a file unpublished, upload it with type=Dokument: onOffice keeps it as internal (category internal), not on the homepage and without portals; images onOffice keeps as external and on the homepage (measured).docm, xlsm, pptm β¦), no SVG/HTML; content matching the extension (PDF, JPG/PNG/GIF/WEBP, DOCX/XLSX/PPTX); file type matching the content (image types only for images, also when changing); document attribute as a key from meta; only characters up to U+FFFF in names, titles and free text. propform downloads source_url itself, only after access and record have been checked β only public https addresses: propform resolves the name once, checks every address and connects only to these; at most 3 redirects (each checked the same way), 60 seconds.502 with message and error.onoffice_errorcode; unknown file or record β 404.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).