propform API (REST)

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).

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.
  • Areas per key: every key has areas with one tick each (set when creating it and changeable later under Settings β†’ API keys): propform features (forms, fields, rules, submissions, account, letterheads, staff, webhooks – on by default), onOffice data (search profiles and files via the official onOffice API – only shown if propform has activated it for your account). New keys only have propform features; keys created before 09.10.2026 keep their previous access, onOffice data is off for them.
  • If a key lacks the area of a request, the API answers 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.
  • 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), 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:

  • 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.
  • The list contains all your forms – including those shared as copy templates. The field form_template indicates that sharing (writable via PATCH, true only with confirm: true); use the template=0/1 filter to filter by it.
  • 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 – 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)

Display variants via API

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_mode is 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 recommend display_variant: "slider" β€” if a field has both set, display_variant wins.

Link field via API

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]}'

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.

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

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

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 folders

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.

Form groups

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).

PDF templates (PDF generator)

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:

  • The response is always the normalised state after saving – exactly what the template editor would display. Unknown keys and invalid field references are silently discarded: if in doubt, compare the response with what you sent.
  • The block list is a snapshot: fields you add via the API after the template has been created do not automatically end up in it. Use regenerate (which replaces your own block customisations!) or add the blocks specifically via PATCH.
  • If you enable settings.document_signing.enabled, propform automatically ensures that the template contains a document signature block (if there is none yet).

Block model reference

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.

Letterheads

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"
  • Encrypted or corrupted PDFs are rejected with 422. Low-resolution images are accepted, but the response then contains a note in the warning field.
  • Assigning to a form: set design.letterhead_id in the PDF template PATCH (account letterheads can be used everywhere, form-private ones only in their own form).

Account settings

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.
  • Fonts (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.
  • Email address (login), password, API keys, certificates and subscription/payment data are not part of the API.
  • The email slots in notifications are stored as a compact list: gaps are closed up after saving.

onOffice configuration

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)

Staff members (staff area)

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
  • The seat limit of your plan (staff_area.seat_limit under /account) is strictly enforced when creating, activating and importing (422). Only active staff members occupy a seat.
  • For onOffice staff members, name, email and groups come from the daily onOffice sync – the email is therefore not changeable via the API.
  • Deactivating (is_active: false) immediately ends all of the staff member's running sessions.
  • If you delete a group that is used in form access settings, it is removed there; if no access entry remains, the form is accessible to nobody (deliberately restrictive) – set the access again in that case.
  • Which forms are protected and who is allowed to see them is controlled via the form fields 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.

onOffice search profiles

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"
}
  • Values: range fields as {"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).
  • Only "Kein Template": the onOffice API only knows the search criteria of this set. propform answers an unknown field, or one only active in a template, with 422 without changing anything in onOffice.
  • KO criteria: 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.
  • Responses return ranges as {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.
  • Errors: onOffice rejects β†’ 502 with message and error.onoffice_errorcode; unknown profile, address or property β†’ 404.
  • Exactly one profile per call; deleted profiles cannot be restored. Deleting probably needs a separate right for the onOffice API user (otherwise 502 with onoffice_errorcode 156; for Marketplace connections, propform support can help). Without advisor, onOffice enters the API user as advisor.

Files of onOffice estates and addresses

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"
  • Addresses: via the API onOffice keeps only the file name there – propform answers title, type, free_text and document_attribute with 422, likewise changing and link files. Uploading, reading and deleting work.
  • Publication: propform sends no release when uploading, portals stay off; 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: only once per estate – if another file has it, propform removes it there first and names that file in the response (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).
  • Checks before sending: no executable files, no Office files with macros (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.
  • Errors: onOffice rejects β†’ 502 with message and error.onoffice_errorcode; unknown file or record β†’ 404.

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).