Skip to content

Form Builder API ​

Base URL: /api/v1/formbuilder

Dynamic forms: templates own sections, sections own fields, and a response is one person's filled-in copy of a template.

Paths changed in August 2026

Every resource is form-*: /form-templates, /form-sections, /form-fields. This page previously documented /templates, /sections and /fields, which have never existed since the Go package was rewritten against the live schema. If you are working from an older copy of these docs, none of its paths resolve.

Two audiences, one API ​

The form builder is used by staff configuring forms and by applicants filling them in, and the split is not by URL — it is per route:

  • Configuration (create, update, delete, reorder, publish) is gated on ADMIN.FORM_BUILDER.
  • Rendering and answering is session-only, with no permission node. An applicant holding a magic-link session carries no role, so a node gate would lock them out of their own application. Ownership is enforced per row inside the service instead, and staff-wide access to other people's responses is checked in-handler against ADMIN.FORM_BUILDER.RESPONSES.

The exempt reads are deliberately narrow: GET /form-sections, GET /form-fields and GET /form-templates/for_program expose form structure, never anyone's answers.

Status values

A template that is live is ACTIVE, not PUBLISHED — the publish action exists, but the resulting status is ACTIVE. The form tables also carry no unique constraints, so uniqueness is not enforced by the database.

Endpoints Overview ​

Templates ​

MethodEndpointDescriptionGate
GET/form-templatesList templatesADMIN.FORM_BUILDER
POST/form-templatesCreate a templateADMIN.FORM_BUILDER
GET/form-templates/{templateID}Get a templateADMIN.FORM_BUILDER
PATCH/form-templates/{templateID}Update a templateADMIN.FORM_BUILDER
DELETE/form-templates/{templateID}Delete a templateADMIN.FORM_BUILDER
POST/form-templates/{templateID}/publishMake the template ACTIVEADMIN.FORM_BUILDER
POST/form-templates/{templateID}/archiveArchive the templateADMIN.FORM_BUILDER
POST/form-templates/{templateID}/duplicateCopy the template with its sections and fieldsADMIN.FORM_BUILDER
GET/form-templates/for_programTemplates attached to a programsession only

Sections ​

MethodEndpointDescriptionGate
GET/form-sectionsList sections (structure, no answers)session only
GET/form-sections/{sectionID}Get a sectionADMIN.FORM_BUILDER
POST/form-sectionsCreate a sectionADMIN.FORM_BUILDER
PATCH/form-sections/{sectionID}Update a sectionADMIN.FORM_BUILDER
DELETE/form-sections/{sectionID}Delete a sectionADMIN.FORM_BUILDER
POST/form-sections/{sectionID}/reorderMove one sectionADMIN.FORM_BUILDER
POST/form-sections/bulk_reorderReorder several at onceADMIN.FORM_BUILDER

Fields ​

MethodEndpointDescriptionGate
GET/form-fieldsList fields (structure, no answers)session only
GET/form-fields/{fieldID}Get a fieldADMIN.FORM_BUILDER
POST/form-fieldsCreate a fieldADMIN.FORM_BUILDER
PATCH/form-fields/{fieldID}Update a fieldADMIN.FORM_BUILDER
DELETE/form-fields/{fieldID}Delete a fieldADMIN.FORM_BUILDER
POST/form-fields/{fieldID}/reorderMove one fieldADMIN.FORM_BUILDER
POST/form-fields/bulk_reorderReorder several at onceADMIN.FORM_BUILDER
POST/form-fields/{fieldID}/set_program_visibilityShow or hide a field per programADMIN.FORM_BUILDER

The GRID field type ​

A field of type GRID collects a table. Its column schema lives in grid_config_json on the field — not in static_options_json, which is already parsed as [{value,label}] by the SELECT/MULTI_SELECT membership check and could not carry per-column options.

json
{
  "version": 1,
  "rows": { "mode": "DYNAMIC", "min": 1, "max": 10, "add_label": "Add a course" },
  "show_totals": true,
  "columns": [
    { "code": "course",  "label": "Course",  "type": "TEXT",   "required": true },
    { "code": "credits", "label": "Credits", "type": "NUMBER", "total": true },
    { "code": "grade",   "label": "Grade",   "type": "SELECT",
      "options": [ { "value": "A", "label": "A" }, { "value": "B", "label": "B" } ] }
  ]
}

rows.mode is DYNAMIC (the user adds rows; min/max/add_label apply) or FIXED (the admin declares them in rows.items as [{code,label}], up to 50, and the user fills the cells). Limits: 20 columns, 200 rows. Cell types are restricted to TEXT, NUMBER, DATE, SELECT and CHECKBOX.

version is validated: absent means 1, and anything else is rejected outright so a future shape change fails loudly instead of being misparsed.

Answers go in form_field_responses.value_json as an array of row objects keyed by column code — the same column MULTI_SELECT already uses:

json
[ { "course": "ENGL 101", "credits": 3, "grade": "A" },
  { "course": "HIST 110", "credits": 3, "grade": "B" } ]

A FIXED grid's rows additionally carry "_row", naming the declared row they answer. _row is reserved — column codes may not begin with an underscore.

[] is not an answer

An empty grid serializes to a non-NULL string, so the "has this been answered?" check was taught to treat [] as unanswered, the same as an empty MULTI_SELECT. A required GRID therefore demands at least one row.

Configuration is validated at design time, when the field is created or updated — bad codes, a SELECT column with no options, min above max, fixed rows on a dynamic grid. That is what keeps submission-time validation honest: anything rejected there can never reach a response.

Program assignments ​

MethodEndpointDescriptionGate
GET/program-form-templatesList template-to-program assignmentsADMIN.FORM_BUILDER
POST/program-form-templatesAttach a template to a programADMIN.FORM_BUILDER
PATCH/program-form-templates/{programFormID}Update an assignmentADMIN.FORM_BUILDER
DELETE/program-form-templates/{programFormID}Remove an assignmentADMIN.FORM_BUILDER

Responses ​

Session only. Ownership is enforced per row in the service; staff reading someone else's response are checked against ADMIN.FORM_BUILDER.RESPONSES inside the handler.

MethodEndpointDescription
GET/form-responsesList responses the caller may see
POST/form-responsesStart a response
GET/form-responses/{responseID}Get a response
PATCH/form-responses/{responseID}Update a response
DELETE/form-responses/{responseID}Delete a response
POST/form-responses/{responseID}/save_progressSave without submitting
POST/form-responses/{responseID}/submitSubmit the response

Node naming ​

The gates were ADMIN.FORM_BUILDER and ADMIN.FORM_BUILDER.RESPONSES until August 2026: the admissions domain was removed but those two nodes were kept, because the form builder still depended on them. Migration 209 finished the job — the screen moved to Administration → System Configuration and the nodes were renamed and re-parented in place to ADMIN.FORM_BUILDER and ADMIN.FORM_BUILDER.RESPONSES. Renaming preserved every grant, because every FK into core.permission_nodes keys on node_id, not on the code. See the unauthenticated surface.




Complete endpoint reference ​

Every route this domain registers, with the permission node that gates it. Generated from the route table, so it is complete by construction — the sections above explain behaviour, this one guarantees nothing is missing.

Paths are relative to /api/v1/formbuilder. A node in italics is an exemption rather than a grant: selfservice acts only on the caller's own record, public needs no session at all, and custom carries its own wrapper.

MethodEndpointPermission nodeDescription
GET/form-fieldsselfserviceList form fields
POST/form-fieldsADMIN.FORM_BUILDERCreate a form field
POST/form-fields/bulk_reorderADMIN.FORM_BUILDERBulk reorder
DELETE/form-fields/{fieldID}ADMIN.FORM_BUILDERDelete a form field
GET/form-fields/{fieldID}ADMIN.FORM_BUILDERGet one form field
PATCH/form-fields/{fieldID}ADMIN.FORM_BUILDERUpdate a form field
POST/form-fields/{fieldID}/reorderADMIN.FORM_BUILDERReorder for a form field
POST/form-fields/{fieldID}/set_program_visibilityADMIN.FORM_BUILDERSet program visibility for a form field
GET/form-responsesselfserviceList form responses
POST/form-responsesselfserviceCreate a form respons
DELETE/form-responses/{responseID}selfserviceDelete a form respons
GET/form-responses/{responseID}selfserviceGet one form respons
PATCH/form-responses/{responseID}selfserviceUpdate a form respons
POST/form-responses/{responseID}/save_progressselfserviceAdd save progress to a form respons
POST/form-responses/{responseID}/submitselfserviceSubmit for a form respons
GET/form-sectionsselfserviceList form sections
POST/form-sectionsADMIN.FORM_BUILDERCreate a form section
POST/form-sections/bulk_reorderADMIN.FORM_BUILDERBulk reorder
DELETE/form-sections/{sectionID}ADMIN.FORM_BUILDERDelete a form section
GET/form-sections/{sectionID}ADMIN.FORM_BUILDERGet one form section
PATCH/form-sections/{sectionID}ADMIN.FORM_BUILDERUpdate a form section
POST/form-sections/{sectionID}/reorderADMIN.FORM_BUILDERReorder for a form section
GET/form-templatesADMIN.FORM_BUILDERList form templates
POST/form-templatesADMIN.FORM_BUILDERCreate a form template
GET/form-templates/for_programselfserviceGet for program
DELETE/form-templates/{templateID}ADMIN.FORM_BUILDERDelete a form template
GET/form-templates/{templateID}ADMIN.FORM_BUILDERGet one form template
PATCH/form-templates/{templateID}ADMIN.FORM_BUILDERUpdate a form template
POST/form-templates/{templateID}/archiveADMIN.FORM_BUILDERArchive for a form template
POST/form-templates/{templateID}/duplicateADMIN.FORM_BUILDERDuplicate for a form template
POST/form-templates/{templateID}/publishADMIN.FORM_BUILDERPublish for a form template
GET/program-form-templatesADMIN.FORM_BUILDERList program form templates
POST/program-form-templatesADMIN.FORM_BUILDERCreate a program form template
DELETE/program-form-templates/{programFormID}ADMIN.FORM_BUILDERDelete a program form template
PATCH/program-form-templates/{programFormID}ADMIN.FORM_BUILDERUpdate a program form template

Student Information System Documentation