Skip to content
Authors
  • Sem Tadema
    Sem TademaCTO

This tutorial shows how public form submission works in Caraer and how to implement a website form flow using the Forms API.

  • Fetch a public form definition
  • Render fields in your website frontend
  • Upload files if needed
  • Submit form values to Caraer
  • Persist submissions as records according to your form configuration

Core endpoints for public forms

From FormController:

  • GET /api/v2/forms/public/{companyUuid}/{formUuid}
  • POST /api/v2/forms/public/{companyUuid}/{formUuid}/upload
  • POST /api/v2/forms/public/{companyUuid}/{formUuid}/submit
  • POST /api/v2/forms/public/{companyUuid}/{propertyUuid}/options

From WebpageController (for CMS-driven pages that include forms and dynamic content):

  • GET /api/v2/webpages/public/{rootSlug}/{slug}

Step 1: Fetch the public form

curl -X GET "https://your-company.caraer.com/api/v2/forms/public/COMPANY_UUID/FORM_UUID"

This returns a public DTO version of the form that your frontend can render.

Understand the form structure before rendering

The public form payload is structured, not just a flat field list. At a high level it contains:

  • object: the target Caraer object this form submits into
  • grids: ordered form sections/steps in case of a wizard otherwise just one grid
  • description: form-level helper text
  • styling: standard, underline, or plain
  • wizard: whether the form is multi-step
  • metadata: arbitrary key-value metadata
  • thankYouMessage: post-submit success message
  • redirectUrl: optional URL to redirect to after submit

How grids are organized

Each item in grids is a section model (FormItem) with:

  • title
  • description
  • type (section or step): Sections represent an introduction or informational "slide" in the form wizard and do not contain any fields for data entry—they are used for welcome screens or instructions only. Steps, by contrast, contain the actual fields to be completed by the user.
  • grid (two-dimensional array)

The grid is important:

  • outer array = rows
  • inner array = cells in a row

Each cell (GridItem) can hold one of:

  • property (actual input field mapped to a property)
  • text (static content/instruction)
  • form (nested form)
  • submitButton (button label)
  • settings (UI and behavior options like required, placeholder, help text)

Example shape (simplified):

{
  "object": { "uuid": "..." },
  "wizard": true,
  "grids": [
    {
      "title": "Personal details",
      "type": "step",
      "grid": [
        [
          { "property": { "uuid": "prop-firstname", "name": "firstname" } },
          { "property": { "uuid": "prop-lastname", "name": "lastname" } }
        ],
        [
          {
            "property": { "uuid": "prop-email", "name": "email" },
            "settings": {
              "required": true,
              "placeholder": "you@example.com",
              "helpText": "We only use this for application updates."
            }
          }
        ],
        [{ "submitButton": "Submit application" }]
      ]
    }
  ]
}

Practical constraints to account for

  • Non-wizard forms support a single grid section.
  • A form should only have one submit button in total.
  • Field validation happens on submit against property type/format rules.

How nested forms work

A grid cell can contain a full nested form instead of a single property. This is used when one submission should create a parent record plus one or more related records — for example, a Candidate form that also collects Work Experience or Education entries inline.

What you receive in the form definition

When a cell holds a nested form, the form key replaces property. The value is a complete nested form payload (with its own object, grids, and fields). The cell also includes settings.relation, which tells Caraer how to link the nested form's record to the parent record after submission.

[
  { "property": { "uuid": "prop-firstname", "name": "firstname" } },
  {
    "form": {
      "uuid": "nested-work-experience-uuid",
      "object": { "uuid": "...", "name": "WorkExperience" },
      "grids": [
        {
          "type": "step",
          "grid": [
            [
              { "property": { "uuid": "prop-company", "name": "company" } },
              { "property": { "uuid": "prop-role", "name": "role" } }
            ]
          ]
        }
      ]
    },
    "settings": {
      "relation": { "uuid": "...", "name": "has_work_experience" }
    }
  }
]

settings.relation is required for nested forms. Without it, the form configuration is invalid.

Rendering nested forms

Treat nested forms as recursive mini-forms:

  1. When you encounter a cell with form, render that nested form's step grid (typically the grid where type is step).
  2. Render nested form fields the same way you render top-level fields (same property types, settings, and options endpoint behavior).
  3. Nested forms can themselves contain nested forms — keep rendering recursively until you reach property cells.

If the same nested form appears more than once in a parent form (for example, two work-experience blocks), give each instance a stable index so field values do not overwrite each other. A common pattern is to namespace fields by nested form UUID and index:

  • Non-wizard parent: nested-work-experience-uuid[0].company, nested-work-experience-uuid[1].company
  • Wizard parent: step-work-history.nested-work-experience-uuid[0].company

Submitting nested form values

On submit, keep the same overall form structure you fetched — do not flatten nested fields to the parent level. Values belong on the nested form's property cells inside form.grids.

For each nested form instance:

  1. Locate the corresponding form cell in the parent grid.
  2. Walk the nested form's grids recursively.
  3. Set value on each nested property cell (and optionally on property.value).

Example submit fragment with one nested form instance:

{
  "property": { "uuid": "prop-firstname", "name": "firstname" },
  "value": "Jane"
},
{
  "form": {
    "uuid": "nested-work-experience-uuid",
    "object": { "uuid": "...", "name": "WorkExperience" },
    "grids": [
      {
        "type": "step",
        "grid": [
          [
            {
              "property": { "uuid": "prop-company", "name": "company" },
              "value": "Acme Corp"
            },
            {
              "property": { "uuid": "prop-role", "name": "role" },
              "value": "Software Engineer"
            }
          ]
        ]
      }
    ]
  },
  "settings": {
    "relation": { "uuid": "...", "name": "has_work_experience" }
  }
}

When the same nested form UUID appears multiple times, preserve grid order and map each instance to the matching index in your frontend state ([0], [1], etc.).

What Caraer does after submit

The backend processes nested forms in two stages:

  1. Parent record — fields on the outer form are validated and saved to the parent form's target object.
  2. Inner forms — for each grid cell containing form, Caraer submits that nested form as its own submission, creates a record for the nested form's object, and links it to the parent record using settings.relation.

This means nested form fields are not stored on the parent record. They become separate related records in your graph.

Nested form pitfalls

  • Omitting settings.relation on nested form cells.
  • Flattening nested values to the parent payload instead of nesting them under form.grids.
  • Reusing the same field namespace for multiple nested form instances, causing values to collide.
  • Expecting nested form properties to appear on the parent record after submit.

Step 2: Render the form in your website

Implementation checklist:

  • Build a field renderer based on form field types
  • Respect required/optional field behavior from the payload
  • For select-like fields that require dynamic options, call the public options endpoint
  • Keep field keys aligned with form property UUIDs/names expected by the API

Step 3: Upload files first (if your form has file inputs)

curl -X POST "https://your-company.caraer.com/api/v2/forms/public/COMPANY_UUID/FORM_UUID/upload" \
  -F "files=@resume.pdf" \
  -F "files=@motivation-letter.pdf"

The API returns uploaded file keys. Include those keys in the final form payload where your file fields expect them.

Step 4: Submit the form

curl -X POST "https://your-company.caraer.com/api/v2/forms/public/COMPANY_UUID/FORM_UUID/submit" \
  -H "Content-Type: application/json" \
  -d '{
  "object": { "uuid": "..." },
  "wizard": true,
  "grids": [
    {
      "title": "Personal details",
      "type": "step",
      "grid": [
        [
          { "property": { "uuid": "prop-firstname", "name": "firstname" }, "value": "Jane" },
          { "property": { "uuid": "prop-lastname", "name": "lastname" }, "value": "Doe" }
        ],
        [
          {
            "property": { "uuid": "prop-email", "name": "email" },
            "settings": {
              "required": true,
              "placeholder": "you@example.com",
              "helpText": "We only use this for application updates."
            },
            "value": "jane.doe@example.com"
          }
        ],
        [{ "submitButton": "Submit application" }]
      ]
    }
  ]
}'

On success, you receive a SuccessResponse containing a submission UUID.

For detailed type/format submission rules (for example semicolon-delimited multi-select/tag values, range format, structure JSON string, and date submission rules), see:

How this connects to records

When you submit a form via the Caraer API, the payload isn't just stored as a flat submission but is processed by the backend FormService. This service performs validation and then creates or updates actual objects (records) in your configured Caraer data model, linking the form data to your business entities.

A special case is when you want to track where or how the conversion happened—for example, relating a candidate submission to a specific web page or campaign. Caraer provides a key called conversionPageRecordUuid for this. If you include conversionPageRecordUuid in your form payload, the backend FormService will use it to create a relation between the submitted record (such as a Candidate or Lead) and the page record where the conversion originated.

To make this work in your integration:

  1. In your form submission payload, add the conversionPageRecordUuid inside the metadata object rather than at the top level. For example:
 {
   // ...existing form data
   "metadata": {
     "conversionPageRecordUuid": "THE_TARGET_PAGE_RECORD_UUID"
   }
 }
  1. Make sure the referenced page record UUID exists and your API token has access.
  2. Caraer will then link the submitted record to this page in the backend automatically as part of the submission.
  3. You can view or filter such relations later via the records API, using relation queries.

Note: Other relations (e.g., linking a record to a vacancy or source) work similarly—the FormService picks up relation fields from the payload or object/form definition and creates the links in the graph as part of post-processing, using the same internal service methods.

For direct manipulation and relation management outside forms, use the Records endpoints as described in the dedicated records guide.

Common pitfalls

  • Submitting files directly inside submit payload without uploading first.
  • Mismatch between frontend field keys and backend form schema.
  • Forgetting to fetch dynamic options for relation/select-like fields.
  • Flattening nested form values instead of nesting them under form.grids (see How nested forms work).