Building forms

How an organization creates, checks, replaces and archives its forms as a whole through the API or an AI agent: a tree of header, pages and elements in one call.

What it is about

A form is a tree: header (name, slug, target, form of address, consents), pages and the elements on them (text, standard field, custom field, matrix, repeater). It is built in one call, not element by element; new custom fields are created along the way. The target is PROSPECT (submissions become prospects) or CLIENT_RECORD (client-record forms, only with the practice license right). All building endpoints require a person with the organization admin role; reading and the dry run need forms:read, creating, replacing, settings, the licence logo, the share link and archiving need forms:write. The personal link needs no admin role, but contacts:write in addition.

1. Fetch the building blocks

GET /forms/building-blocks?targetEntity=PROSPECT lists the standard fields (key, and how they are named in conditions: inConditions), the organization's existing custom fields with slug, type and options, the types for new fields, the condition language and the structure of matrix and repeater. An existing custom field is used by its slug instead of being created again.

curl
curl "https://www.claritos.io/api/v1/forms/building-blocks?targetEntity=PROSPECT" \
  -H "Authorization: Bearer capi_…"

2. Describe the tree

pages[].elements[] with kind: text (content), standardField (field = key from step 1), field (field = slug; with new the field is created: label, type, options), matrix and repeater (key, config). Each field can carry required, question, placeholder and condition; condition is an expression over fields FURTHER UP, e.g. sleep == 'poor'. Fields with the same row stand side by side, weight splits the width. Consents: consentMode none, optional or promotion; consents.marketing.email and .sms with your own wording or ORGANIZATION for the organization's text, consents.additional for up to two further checkboxes. The inquiry itself needs no checkbox; the form shows a privacy notice.

json
{
  "targetEntity": "PROSPECT",
  "name": "Vorab-Check Schlaf",
  "slug": "vorab-check-schlaf",
  "addressForm": "SIE",
  "consentMode": "none",
  "pages": [
    {
      "title": "Vor Ihrem Termin",
      "elements": [
        { "kind": "standardField", "field": "firstName", "required": true, "row": "name" },
        { "kind": "standardField", "field": "lastName", "required": true, "row": "name" },
        { "kind": "standardField", "field": "email", "required": true },
        { "kind": "field", "field": "schlaf", "required": true,
          "new": { "label": "Wie schlafen Sie?", "type": "SELECT", "options": ["gut", "mittel", "schlecht"] } },
        { "kind": "field", "field": "schlaf_notiz", "condition": "schlaf == 'schlecht'",
          "new": { "label": "Was stört Ihren Schlaf?", "type": "TEXTAREA" } }
      ]
    }
  ]
}

3. Dry run

POST /forms/validate checks the tree like creating it, without saving anything, and always answers 200: valid, errors with path (e.g. body.pages.0.elements.3.field, counted from 0), code and sentence, plus notes and the fields that will be created (createdFields) or reused (reusedFields). A slug that is already taken appears here as an error with the code form-slug-taken at body.slug. Green means: creating will succeed.

curl
curl -X POST https://www.claritos.io/api/v1/forms/validate \
  -H "Authorization: Bearer capi_…" -H "Content-Type: application/json" \
  -d @vorab-check.json
json
{ "valid": true, "errors": [], "notes": [], "slug": "vorab-check-schlaf",
  "pages": 1, "elements": 5, "createdFields": ["schlaf", "schlaf_notiz"], "reusedFields": [] }

4. Create

POST /forms creates the form and answers 201 with the definition as in GET /forms/{id}, plus build with createdFields, reusedFields and notes. Errors in the tree come as 422 validation-failed with the same paths as in the dry run, a taken slug as 409 with reason form-slug-taken. An Idempotency-Key prevents a second form when the call is repeated.

curl
curl -X POST https://www.claritos.io/api/v1/forms \
  -H "Authorization: Bearer capi_…" -H "Content-Type: application/json" \
  -H "Idempotency-Key: <eindeutig>" -d @vorab-check.json

5. Replace as a whole

PUT /forms/{id} takes the same body and replaces the tree. Anything missing from the body counts as the default, not as the previous value: always send the whole tree, ideally derived from GET /forms/{id}. What the body does not know (sharing, links, completion page, display) stays; PATCH changes it (section 6). Pages are kept by position, submissions stay readable. The target cannot be changed (422 at body.targetEntity); an archived form answers 409 with reason archived.

curl
curl -X PUT https://www.claritos.io/api/v1/forms/<FORMULAR> \
  -H "Authorization: Bearer capi_…" -H "Content-Type: application/json" \
  -d @vorab-check.json

6. Settings and share link

PATCH /forms/{id} changes only settings, not the tree: name, addressForm, allowMultiple, publicAccessEnabled (public link), availableInPortal, consentMode, the display (showProgress, showSummary, showFieldLabels, showClientHeader), reminderService, recognitionCard, translateResponses, thirdPartyMode, addressFinderMode and addressFinderPlaceholder, licenseLabel and completion. Anything missing stays; the names are the same as in GET /forms/{id}, and the response is the definition. completion replaces the whole completion page, null restores the default text. Slug, target, pages and consents are set only with PUT; sending them returns 422 invalid_combination. GET /forms/{id}/share-link reads the organization's share link (url is null while none has been created or the public link is off). POST /forms/{id}/share-link creates it and returns the same link on every further call; if the public link is off, it answers 409 with reason public-access-disabled. Affiliate links never appear here: every submission through them would be attributed to the affiliate. The licence logo takes two steps: POST /forms/{id}/license-logo/upload returns uploadUrl, uploadHeaders and storageKey; the file (PNG, JPEG, WebP or SVG, at most 2 MB) goes to uploadUrl with PUT; then POST /forms/{id}/license-logo/confirm confirms it with the storageKey. Only then does the form show the logo (licenseLogoUrl in GET /forms/{id}); if the file has not arrived yet, confirm answers 422 at body.storageKey. DELETE /forms/{id}/license-logo removes it.

curl
curl -X PATCH https://www.claritos.io/api/v1/forms/<FORMULAR> \
  -H "Authorization: Bearer capi_…" -H "Content-Type: application/json" \
  -d '{ "publicAccessEnabled": true, "showSummary": true, "completion": { "title": "Danke!" } }'
curl
curl -X POST https://www.claritos.io/api/v1/forms/<FORMULAR>/share-link \
  -H "Authorization: Bearer capi_…"

7. Personal link

POST /forms/{id}/personal-links issues a link for one particular person, for example after a business card at a trade fair booth. values carries their details with std: keys as when submitting; at least std:email, std:firstName or std:lastName is required, otherwise 422 invalid_combination at body.values. The contact is created or found on the way, by the same duplicate rule as when submitting; empty fields are filled in, nothing is overwritten. The form shows only the values sent, nothing already stored on the contact. The response is 201 with url, contactId and validUntil (seven days); every call is a new link. documentId attaches a document of the organization (such as the photo of the card) to the contact if it is not assigned to anyone yet; an unknown document returns 422 invalid_reference. Only for forms with target PROSPECT, otherwise 409 with reason target-not-supported. Scopes forms:write and contacts:write, no admin role.

curl
curl -X POST https://www.claritos.io/api/v1/forms/<FORMULAR>/personal-links \
  -H "Authorization: Bearer capi_…" -H "Content-Type: application/json" \
  -d '{ "values": { "std:firstName": "Anna", "std:lastName": "Muster", "std:email": "anna@example.com" } }'
json
{ "url": "https://www.claritos.io/f/Zk3q…", "contactId": "cmf2contact00000000000001", "validUntil": "2026-11-03T10:00:00.000Z" }

8. Archive and restore

POST /forms/{id}/archive and POST /forms/{id}/unarchive answer 200 with the form; a second call changes nothing. An archived form accepts no submissions; replacing it, changing its settings, the licence logo, the share link and personal links answer 409 with reason archived until it is restored.

With an AI agent

Through the API MCP the same way: first this guide, then the building blocks, repeat the dry run until valid is true, and only then create. For changes to the tree, rebuild it from the definition and replace it as a whole; settings alone are changed by forms.updateSettings, without the tree.

json
{ "name": "guide", "arguments": { "topic": "form-building" } }
{ "name": "call_endpoint", "arguments": { "id": "forms.buildingBlocks", "query": { "targetEntity": "PROSPECT" } } }
{ "name": "call_endpoint", "arguments": { "id": "forms.validate", "body": { … } } }
{ "name": "call_endpoint", "arguments": { "id": "forms.create", "body": { … }, "idempotencyKey": "…" } }