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.
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 |
POST |
/forms/{id}/copy |
Copy form (including fields, rules and email configuration); the copy starts in an inactive state |
The form is a resource in the API: you can also read and write the settings for email sending and ChatGPT integration directly on the form. The field names correspond exactly to the settings in the form editor.
curl -X PATCH https://propform.io/api/v1/forms/123 \
-H "Authorization: Bearer pf_live_DEIN_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Neuer Titel", "active": true}'
Important points to note:
"active": true) only works as long as your planβs limit on active forms has not been reached (otherwise 422). Deactivation is always possible.slug, counters and template flags. Passwords (e.g. form password) are writable, but never appear in responses.form_template indicates that sharing; 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.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.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).