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.