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.
  • 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
POST /forms/{id}/copy Copy form (including fields, rules and email configuration); the copy starts in an inactive state

The form is a resource in the API: you can also read and write the settings for email sending and ChatGPT integration directly on the form. The field names correspond exactly to the settings in the form editor.

curl -X PATCH https://propform.io/api/v1/forms/123 \
  -H "Authorization: Bearer pf_live_DEIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title": "Neuer Titel", "active": true}'

Important points to note:

  • PATCH semantics: Fields that you do not include in the request remain unchanged. This means you can safely modify individual settings.
  • Activate ("active": true) only works as long as your plan’s limit on active forms has not been reached (otherwise 422). Deactivation is always possible.
  • Forms cannot be deleted via the API – this is deliberately restricted to the dashboard.
  • Items that are not writable include slug, counters and template flags. Passwords (e.g. form password) are writable, but never appear in responses.
  • The list contains all your forms – including those shared as copy templates. The read-only field form_template indicates that sharing; 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.
  • 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.

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