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

## What you are building

- 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


```bash
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):


```json
{
  "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.


```json
[
  { "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:


```json
{
  "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)


```bash
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


```bash
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 to submit values by property type and format](/blog/2026-03-25-save-records#how-to-submit-values-by-property-type-and-format)


## 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:



```json
 {
   // ...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](#how-nested-forms-work)).


## Related reading

- [Save records](/blog/2026-03-25-save-records)
- [API reference](/apis)