Skip to content

REST API Forms

Ed Mozley edited this page Jul 3, 2026 · 1 revision

πŸ“ 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": "manager@example.com", "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.

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally