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
From FormController:
GET /api/v2/forms/public/{companyUuid}/{formUuid}POST /api/v2/forms/public/{companyUuid}/{formUuid}/uploadPOST /api/v2/forms/public/{companyUuid}/{formUuid}/submitPOST /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}
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.
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 intogrids: ordered form sections/steps in case of a wizard otherwise just one griddescription: form-level helper textstyling:standard,underline, orplainwizard: whether the form is multi-stepmetadata: arbitrary key-value metadatathankYouMessage: post-submit success messageredirectUrl: optional URL to redirect to after submit
Each item in grids is a section model (FormItem) with:
titledescriptiontype(sectionorstep): 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" }]
]
}
]
}- 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.
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.
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.
Treat nested forms as recursive mini-forms:
- When you encounter a cell with
form, render that nested form's step grid (typically the grid wheretypeisstep). - Render nested form fields the same way you render top-level fields (same property types, settings, and options endpoint behavior).
- 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
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:
- Locate the corresponding
formcell in the parent grid. - Walk the nested form's grids recursively.
- Set
valueon each nested property cell (and optionally onproperty.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.).
The backend processes nested forms in two stages:
- Parent record — fields on the outer form are validated and saved to the parent form's target object.
- 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 usingsettings.relation.
This means nested form fields are not stored on the parent record. They become separate related records in your graph.
- Omitting
settings.relationon 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.
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
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.
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:
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:
- In your form submission payload, add the
conversionPageRecordUuidinside themetadataobject rather than at the top level. For example:
{
// ...existing form data
"metadata": {
"conversionPageRecordUuid": "THE_TARGET_PAGE_RECORD_UUID"
}
}- Make sure the referenced page record UUID exists and your API token has access.
- Caraer will then link the submitted record to this page in the backend automatically as part of the submission.
- 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.
- 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).