Forms
Dynamic form builder endpoints. Forms are created and managed through the admin, and rendered dynamically on the frontend based on their field structure. Submissions are stored in the database and can be reviewed in the admin.
Base path: /api/v1/forms
Public Endpoints
Get Form by Slug
Returns the full structure of an active form including fields and translations. No authentication required. Used by the frontend to render the form dynamically.
Path parameters
| Parameter | Type | Description |
|---|---|---|
slug |
string | Unique form slug |
Response 200 OK
{
"id": "uuid",
"slug": "contact",
"is_active": true,
"notify_emails": ["admin@site.com"],
"created_by": "uuid",
"translations": [
{
"id": "uuid",
"form_id": "uuid",
"language_id": "uuid",
"name": "Contact Us",
"description": "Send us a message and we will get back to you.",
"success_message": "Thank you! We will be in touch soon.",
"submit_label": "Send Message",
"created_at": "2026-01-01T00:00:00Z",
"updated_at": null
}
],
"fields": [
{
"id": "uuid",
"form_id": "uuid",
"name": "email",
"type": "email",
"is_required": true,
"order": 0,
"options": null,
"translations": [
{
"id": "uuid",
"field_id": "uuid",
"language_id": "uuid",
"label": "Email Address",
"placeholder": "you@example.com",
"help_text": null,
"created_at": "2026-01-01T00:00:00Z",
"updated_at": null
}
],
"created_at": "2026-01-01T00:00:00Z",
"updated_at": null
}
],
"created_at": "2026-01-01T00:00:00Z",
"updated_at": null
}
Errors
| Status | Description |
|---|---|
404 |
Form not found or not active |
Submit Form
Submits a filled form. Validates required fields and field types server-side. No authentication required.
Path parameters
| Parameter | Type | Description |
|---|---|---|
slug |
string | Unique form slug |
Request body
{
"data": {
"email": "user@example.com",
"name": "John Doe",
"message": "Hello, I would like to get in touch."
}
}
| Field | Type | Required | Description |
|---|---|---|---|
data |
object | ✅ | Key-value pairs where keys match field name values |
Response 201 Created
{
"id": "uuid",
"form_id": "uuid",
"data": {
"email": "user@example.com",
"name": "John Doe",
"message": "Hello, I would like to get in touch."
},
"ip_address": "192.168.1.1",
"is_read": false,
"created_at": "2026-01-01T00:00:00Z"
}
Server-side validation
The backend validates submissions against the form's field definitions:
- Required fields must have a non-empty value
emailfields must match a valid email formatnumberfields must be numericselectandradiofields must contain a value defined inoptions
Errors
| Status | Description |
|---|---|
400 |
Validation failed (required, type, etc) |
404 |
Form not found or not active |
Admin — Forms
List Forms
Required permission: forms:read
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
int | 1 |
Page number |
per_page |
int | 20 |
Results per page |
Response 200 OK
{
"data": [...],
"pagination": {
"total": 5,
"page": 1,
"per_page": 20,
"pages": 1,
"has_next": false,
"has_prev": false
}
}
Create Form
Required permission: forms:create
Request body
{
"slug": "contact",
"is_active": true,
"notify_emails": ["admin@site.com"],
"translations": [
{
"language_id": "uuid",
"name": "Contact Us",
"description": "Send us a message.",
"success_message": "Thank you!",
"submit_label": "Send"
}
],
"fields": [
{
"name": "email",
"type": "email",
"is_required": true,
"order": 0,
"options": null,
"translations": [
{
"language_id": "uuid",
"label": "Email Address",
"placeholder": "you@example.com",
"help_text": null
}
]
}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
slug |
string | ✅ | 2–255 characters, unique |
is_active |
boolean | ❌ | Default: true |
notify_emails |
array | ❌ | List of email addresses for notifications |
translations |
array | ❌ | Form translations |
fields |
array | ❌ | Initial fields to create with the form |
Response 201 Created — full FormResponse object.
Errors
| Status | Description |
|---|---|
409 |
Slug already exists |
Get Form
Required permission: forms:read
Response 200 OK — full FormResponse object.
Errors
| Status | Description |
|---|---|
404 |
Form not found |
Update Form
Required permission: forms:update
Request body — all fields optional
{
"slug": "contact-us",
"is_active": false,
"notify_emails": ["admin@site.com", "support@site.com"]
}
Response 200 OK — updated FormResponse object.
Errors
| Status | Description |
|---|---|
404 |
Form not found |
409 |
Slug already exists |
Delete Form
Soft-deletes a form and all its fields. Submissions are also removed.
Required permission: forms:delete
Response 204 No Content
Errors
| Status | Description |
|---|---|
404 |
Form not found |
Admin — Form Translations
Upsert Form Translation
Creates or updates a translation for a specific language.
Required permission: forms:update
Request body
{
"language_id": "uuid",
"name": "Contact Us",
"description": "Send us a message.",
"success_message": "Thank you! We will be in touch.",
"submit_label": "Send Message"
}
| Field | Type | Required | Description |
|---|---|---|---|
language_id |
UUID | ✅ | Must exist in DB |
name |
string | ✅ | Form name shown in UI |
description |
string | ❌ | Text shown above the form |
success_message |
string | ❌ | Message shown after submission |
submit_label |
string | ❌ | Text on the submit button |
Response 200 OK — updated FormResponse object.
Delete Form Translation
Required permission: forms:update
Response 200 OK — updated FormResponse object.
Errors
| Status | Description |
|---|---|
404 |
Form not found |
404 |
Translation not found |
Admin — Form Fields
Add Field
Required permission: forms:update
Request body
{
"name": "phone",
"type": "tel",
"is_required": false,
"order": 2,
"options": null,
"translations": [
{
"language_id": "uuid",
"label": "Phone Number",
"placeholder": "+381 11 123 456",
"help_text": "Optional"
}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | ✅ | Technical key, unique per form (e.g. email, phone) |
type |
string | ✅ | See field types below |
is_required |
boolean | ❌ | Default: false |
order |
int | ❌ | Display order, default: 0 |
options |
array | ❌ | Required for select, radio, checkbox types |
translations |
array | ❌ | Field label, placeholder, help text per language |
Response 201 Created — updated FormResponse object.
Errors
| Status | Description |
|---|---|
404 |
Form not found |
409 |
Field name already exists on this form |
Update Field
Required permission: forms:update
Request body — all fields optional
Response 200 OK — updated FormResponse object.
Delete Field
Required permission: forms:update
Response 200 OK — updated FormResponse object.
Reorder Fields
Updates the display order of multiple fields in a single request. Used for drag-and-drop reordering in the admin UI.
Required permission: forms:update
Request body
{
"fields": [
{ "field_id": "uuid-1", "order": 0 },
{ "field_id": "uuid-2", "order": 1 },
{ "field_id": "uuid-3", "order": 2 }
]
}
Response 200 OK — updated FormResponse object.
Errors
| Status | Description |
|---|---|
404 |
One or more field IDs not found |
Admin — Field Translations
Upsert Field Translation
Required permission: forms:update
Request body
{
"language_id": "uuid",
"label": "Email Address",
"placeholder": "you@example.com",
"help_text": "We will never share your email."
}
| Field | Type | Required | Description |
|---|---|---|---|
language_id |
UUID | ✅ | Must exist in DB |
label |
string | ✅ | Visible field label |
placeholder |
string | ❌ | Input placeholder text |
help_text |
string | ❌ | Helper text shown below the field |
Response 200 OK — updated FormResponse object.
Delete Field Translation
Required permission: forms:update
Response 200 OK — updated FormResponse object.
Admin — Submissions
List Submissions
Required permission: forms:read
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
int | 1 |
Page number |
per_page |
int | 20 |
Results per page |
is_read |
boolean | — | Filter by read status |
Response 200 OK
{
"data": [
{
"id": "uuid",
"form_id": "uuid",
"data": {
"email": "user@example.com",
"message": "Hello!"
},
"ip_address": "192.168.1.1",
"is_read": false,
"created_at": "2026-01-01T00:00:00Z"
}
],
"pagination": {
"total": 10,
"page": 1,
"per_page": 20,
"pages": 1,
"has_next": false,
"has_prev": false
},
"unread": 3
}
Unread count
The unread field always reflects the total number of unread submissions for this form,
regardless of the current is_read filter. Use it to display a badge in the admin UI.
Mark Submission as Read
Required permission: forms:update
Response 200 OK — updated FormSubmissionResponse object.
Errors
| Status | Description |
|---|---|
404 |
Submission not found |
Delete Submission
Required permission: forms:delete
Response 204 No Content
Errors
| Status | Description |
|---|---|
404 |
Submission not found |
Field Types
| Type | Description | options required |
|---|---|---|
text |
Single-line text input | ❌ |
email |
Email input with format validation | ❌ |
tel |
Telephone number input | ❌ |
textarea |
Multi-line text input | ❌ |
number |
Numeric input with type validation | ❌ |
select |
Dropdown, single selection | ✅ |
radio |
Radio buttons, single selection | ✅ |
checkbox |
Checkbox group, multiple selection | ✅ |
Options format
For select, radio, and checkbox fields, options must be an array of objects: