REST API Forms - edmozley/freeitsm GitHub Wiki
๐ REST API: Forms
The complete usage guide for the Forms module of the REST API โ custom forms, their version chains, and submissions. Mirrors the interactive documentation at System โ API โ Documentation (with its live "Try it" tester).
[!NOTE] New to the API? The basics table on REST API: Tickets covers base URL, authentication, the response envelope, error codes, rate limits and PATCH semantics โ identical across all modules.
What's distinctive about Forms:
| ๐ Version chains | A form's history is a chain of rows โ each new version points back at its predecessor, the leaf is the only editable version, older ones are frozen snapshots. GET /forms lists one row per chain (the current version); PATCH/fork/delete on a frozen version is a 409. |
โ๏ธ The form.submitted workflow event |
Submitting via the API fires the exact same workflow dispatch as the UI, with the label-keyed answers map โ so the marquee "new starter form โ tickets in IT/HR/Facilities" automation works identically whether a human or a script submits. |
| ๐งฌ Field ids survive edits | Updating fields uses the module's positional sync: existing field ids are reused in order, so historical submission data stays mapped. Removed fields lose their submission data โ fork a new version instead if you need to restructure without losing history. |
๐ is_active, finally settable |
Nothing in the UI writes is_active today โ the API is currently the only way to retire a form (stop submissions) without deleting it. |
| ๐ฆ Native JSON values | Submit checkbox booleans as booleans and multi-select answers as arrays โ no pre-encoding. Answers come back with checkboxes decoded to arrays too. |
๐ Quick start โ create, submit, read back
B="https://your-server/api/v1"; K="Authorization: Bearer fitsm_โฆ"
# Create a form
FORM=$(curl -s -X POST "$B/forms" -H "$K" -H "Content-Type: application/json" -d '{
"title": "New starter",
"fields": [
{ "field_type": "text", "label": "Full name", "is_required": true },
{ "field_type": "email", "label": "Manager email", "is_required": true },
{ "field_type": "dropdown", "label": "Department", "options": ["IT", "HR", "Finance"] },
{ "field_type": "checkboxes", "label": "Equipment", "options": ["Laptop", "Phone"] }
]}' | jq '.data')
ID=$(echo "$FORM" | jq -r '.id')
# Submit it (field ids from the create response; arrays and booleans are fine as-is)
curl -s -X POST "$B/forms/$ID/submissions" -H "$K" -H "Content-Type: application/json" -d '{
"data": { "1": "Sam Jones", "2": "[email protected]", "3": "IT", "4": ["Laptop", "Phone"] }}'
# Read the answers back, labels attached
curl -s "$B/forms/$ID/submissions" -H "$K"
๐ Forms
๐ข GET /forms ย ๐ forms.read
One row per version chain โ the current version, with field_count and submission_count. is_active=true|false filters; q searches titles.
๐ต POST /forms ย ๐ forms.create
title required; fields is an ordered array of {field_type, label, options?, is_required?}.
Field types: text, textarea, email, number, checkbox (single yes/no), checkboxes (multi-select), dropdown, radio. The last three take an options array. Unknown types and empty labels are a 422 (the UI silently stores/drops them).
๐ข GET /forms/{id} ย ๐ forms.read
The form with its fields and version info:
{ "data": { "id": 7, "title": "New starter", "is_active": true,
"version": { "number": 2, "parent_form_id": 6, "is_current": true },
"fields": [ { "id": 31, "field_type": "text", "label": "Full name", "options": null,
"is_required": true, "sort_order": 0 } ] } }
๐ PATCH /forms/{id} ย ๐ forms.update
In-place save of the current version (a frozen historical version is a 409). Send only what changes โ title, description, is_active, and/or the full fields array (positional sync: existing ids reused in order, extras appended, trailing leftovers deleted with their submission data).
๐ด DELETE /forms/{id} ย ๐ forms.delete
Deletes one version and its submissions โ leaf only. Beware: deleting the leaf resurfaces the previous version as current (that's the UI's behaviour too). A mid-chain id is a 409. Pass ?chain=true to delete the entire version chain and every submission, transactionally โ the "actually delete the form" verb.
๐ Versions
๐ข GET /forms/{id}/versions ย ๐ forms.read
Every version in the chain containing this form (any member's id works), oldest first โ version.is_current marks the editable leaf.
๐ต POST /forms/{id}/versions ย ๐ forms.create
Fork the current version: clones title, description and fields into a new editable version (version_number + 1); the source freezes. Forking from a non-leaf is a 409. Use this instead of PATCH when restructuring a form whose submission history you want to keep intact.
๐จ Submissions
๐ต POST /forms/{id}/submissions ย ๐ form_submissions.create
data maps field id โ value. Validation matches the UI exactly: required fields (a required checkbox must be truthy, required checkboxes must tick at least one), email format, numeric check. Improvements for machines: unknown field ids are a 422 (the UI inserts them blindly), an inactive form is a 409, and booleans/arrays are accepted natively.
On success (201) the form.submitted workflow event fires with the label-keyed answers map and the first email-type answer surfaced as submission.email โ identical to a UI submission.
๐ข GET /forms/{id}/submissions ย ๐ form_submissions.read
Newest first, paginated; submitted_since / submitted_before bound the window. Each submission carries its answers joined to field definitions:
{ "data": [ { "id": 12, "form": { "id": 7, "title": "New starter" },
"submitted_by": { "id": 1, "name": "Administrator" }, "submitted_at": "2026-07-03T09:15:00Z",
"answers": [
{ "field_id": 31, "label": "Full name", "field_type": "text", "value": "Sam Jones" },
{ "field_id": 34, "label": "Equipment", "field_type": "checkboxes", "value": ["Laptop", "Phone"] } ] } ] }
๐ข GET ยท ๐ด DELETE /forms/{id}/submissions/{submission_id} ย ๐ form_submissions.read/delete
One submission (404 if it doesn't belong to that form). DELETE removes it and its answers transactionally.
๐ Permissions
| Group | Actions |
|---|---|
Forms (forms) |
read ยท create (incl. version forks) ยท update ยท delete |
Form submissions (form_submissions) |
read ยท create (the submit verb) ยท delete |
Forms are install-wide โ no company scoping (matches the UI) โ and the module has no audit trail. The AI form-generation endpoints stay UI-only.