REST API Changes - edmozley/freeitsm GitHub Wiki

🟠 REST API: Changes

The complete usage guide for the Change Management module of the REST API β€” every URL, method, parameter and response shape, including the full CAB approval workflow. 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 Changes:

🌍 Install-wide Changes have no company β€” a key's company scope does not restrict them (matches the UI).
🎲 Risk is computed Send risk_likelihood and risk_impact_score (1–5 each); the API computes score = likelihood Γ— impact and bands the level exactly like the UI: ≀4 Low Β· ≀9 Medium Β· ≀15 High Β· ≀20 Very High Β· else Critical.
πŸ—³οΈ CAB voting works over the API The key's acts-as analyst can vote, and the auto-transition mechanics are the UI's own: any required Reject sends the change back to Draft; the all/majority threshold of required members flips it to Approved.
🧾 Strict where it matters Unknown status/type/priority/impact names are a 422 (the UI silently falls back to defaults β€” machines deserve an error).

πŸš€ Quick start

Raise a change, staff its CAB, submit it, and approve it β€” entirely over the API:

B="https://your-server/api/v1"; K="Authorization: Bearer fitsm_…"

# 1. Create (Draft by default; risk computed server-side)
CHG=$(curl -s -X POST "$B/changes" -H "$K" -H "Content-Type: application/json" -d '{
  "title": "Replace SFP on core switch uplink",
  "change_type": "Normal", "priority": "High",
  "reason_for_change": "Fixes PRB-00005 (recurring VPN drops).",
  "risk_likelihood": 2, "risk_impact_score": 4,
  "cab_required": true, "cab_approval_type": "all",
  "work_start_at": "2026-07-05T06:00:00Z", "work_end_at": "2026-07-05T08:00:00Z"
}' | jq -r '.data.id')

# 2. Staff the CAB
curl -s -X POST "$B/changes/$CHG/cab" -H "$K" -H "Content-Type: application/json" \
  -d '{"members": [{"analyst_id": 1, "is_required": true}, {"analyst_id": 2, "is_required": false}]}'

# 3. Submit for approval
curl -s -X PATCH "$B/changes/$CHG" -H "$K" -H "Content-Type: application/json" -d '{"status": "Pending Approval"}'

# 4. A CAB member's key casts the deciding vote β†’ auto-Approved, workflow fires
curl -s -X POST "$B/changes/$CHG/cab/vote" -H "$K" -H "Content-Type: application/json" \
  -d '{"vote": "Approve", "comment": "Rollback plan looks solid."}'

Every step lands in the change's audit trail exactly as if it happened in the UI. 🧾


🟠 Changes

🟒 GET /changes β€” list / search Β  πŸ”‘ changes.read

Parameter Description
state open | closed | all (default all) β€” closed = Rejected/Completed/Failed/Cancelled
status / status_id e.g. Pending Approval, by name or id
change_type / change_type_id Standard / Normal / Emergency
priority / priority_id Β· impact / impact_id By name or id
category_id Β· requester_id Β· assigned_to_id Β· approver_id More id filters
cab_required true / false
risk_level Low, Medium, High, Very High, Critical
q Search title β€” CHG-0042 (or chg42) finds a change by its reference
work_start_from / work_start_to πŸ“… Scheduled-work window (ISO 8601) β€” the calendar shape
created_since / modified_since ISO 8601 date-time bounds
sort created_at (default, desc), modified_at, id, title, work_start_at, risk_score, priority, status β€” prefix - for descending
page / per_page Pagination β€” default 1 / 25, max 100

The change shape (lists; detail adds bodies, PIR, attachments, linked problems):

{ "id": 11, "change_number": "CHG-0011",
  "title": "Replace SFP on core switch uplink",
  "change_type": { "id": 2, "name": "Normal" },
  "status":      { "id": 4, "name": "Approved", "is_closed": false },
  "priority":    { "id": 3, "name": "High" },
  "impact":      { "id": 2, "name": "Medium" },
  "category":    null,
  "requester": null, "assigned_to": { "id": 1, "name": "Administrator" }, "approver": null,
  "approval_at": "2026-07-02T23:30:32Z",
  "cab":  { "required": true, "approval_type": "all" },
  "risk": { "likelihood": 2, "impact": 4, "score": 8, "level": "Medium" },
  "schedule": { "work_start_at": "2026-07-05T06:00:00Z", "work_end_at": "2026-07-05T08:00:00Z",
                "outage_start_at": null, "outage_end_at": null },
  "created_by": { "id": 1, "name": "Administrator" },
  "created_at": "2026-07-02T23:30:12Z", "modified_at": "2026-07-02T23:30:32Z" }

πŸ”΅ POST /changes β€” create Β  πŸ”‘ changes.create

Only title is required. Omitted lookups get the module defaults (Normal / Draft / Medium / Medium); a creation entry is written to the audit trail. Returns 201.

Field group Fields
Lookups (name or id) change_type Β· status Β· priority Β· impact Β· category_id
People (analyst ids) requester_id Β· assigned_to_id Β· approver_id
Schedule (ISO 8601) work_start_at Β· work_end_at Β· outage_start_at Β· outage_end_at
Plans (long text) description Β· reason_for_change Β· risk_evaluation Β· test_plan Β· rollback_plan
Risk (1–5 each) risk_likelihood Β· risk_impact_score β†’ score + level computed
CAB cab_required (bool) Β· cab_approval_type (all | majority)

🟒 GET /changes/{id} β€” get one Β  πŸ”‘ changes.read

Everything above plus description, reason_for_change, risk.evaluation, test_plan, rollback_plan, the PIR block (review, was_successful, actual_start_at/end_at, lessons_learned, follow_up), the attachments list (name/size/type β€” files are managed in the UI), and linked_problems (problems this change fixes).

🟠 PATCH /changes/{id} β€” update Β  πŸ”‘ changes.update

Everything creatable is patchable, plus the PIR fields (pir_was_successful, pir_actual_start_at, pir_actual_end_at, pir_lessons_learned, pir_follow_up). Send only what changes; null clears; identical PATCHes are idempotent.

Audit parity with the UI: human field labels ("Title", "Status", "Work Start", …), display names for lookups, (empty) placeholders, status changes marked status_change β€” and the long plan bodies update silently (the UI doesn't audit them either). Changing risk inputs recomputes score + level. A genuine transition into Approved fires the change.approved workflow event.

[!TIP] The lifecycle is: Draft β†’ Pending Approval β†’ Approved β†’ Scheduled β†’ In Progress β†’ Completed / Failed (Rejected and Cancelled are terminal). Only the Pending Approval β†’ Approved/Draft leg is automated (by CAB votes); every other transition is an explicit status PATCH, exactly like the UI.

πŸ”΄ DELETE /changes/{id} β€” delete Β  πŸ”‘ changes.delete

[!WARNING] Permanent. Deletes the change, removes its attachment files from disk, and cascades comments, CAB entries and the audit trail β€” same as the UI. No restore.


πŸ—³οΈ CAB (Change Advisory Board)

🟒 GET /changes/{id}/cab Β  πŸ”‘ change_cab.read

The roster with votes, plus live approval progress:

{ "data": { "cab_required": true, "approval_type": "all",
    "members": [ { "analyst_id": 1, "name": "Administrator", "is_required": true,
                   "vote": "Approve", "vote_comment": "Rollback plan looks solid.",
                   "voted_at": "2026-07-02T23:30:32Z" } ],
    "progress": { "required_total": 1, "required_approved": 1, "required_rejected": 0 } } }

πŸ”΅ POST /changes/{id}/cab β€” set the roster Β  πŸ”‘ change_cab.manage

Body: {"members": [{"analyst_id": 1, "is_required": true}, …]} β€” replaces the roster. Additions, removals and required↔optional switches are diffed and each writes its own audit entry (like the UI). Returns the updated roster.

πŸ”΅ POST /changes/{id}/cab/vote β€” cast a vote Β  πŸ”‘ change_cab.vote

Body: {"vote": "Approve" | "Reject" | "Abstain", "comment": "…"}. The vote is cast as the analyst the key acts as β€” they must be a CAB member who hasn't voted (403 if not a member, 409 if already voted).

The auto-transition is the UI's exact mechanics, and only runs while the change is in Pending Approval:

  • πŸ”΄ Any required member votes Reject β†’ the change goes back to Draft.
  • 🟒 The threshold of required members approve (all = everyone; majority = more than half) β†’ the change becomes Approved, approval_at is stamped, and the change.approved workflow event fires.
  • βšͺ Optional members' votes are recorded but don't count toward the tally; Abstain never transitions anything.

The response tells you what happened: {"vote": "Approve", "status_changed": true, "new_status": "Approved"}.


πŸ’¬ Comments & history

🟒 GET /changes/{id}/comments Β  πŸ”‘ change_comments.read

Comments newest first, with the author.

πŸ”΅ POST /changes/{id}/comments Β  πŸ”‘ change_comments.create

Body: {"text": "…"}. Always internal, attributed to the acts-as analyst, with a preview written to the audit trail. Comments can't be edited (parity with the UI) β€” only added and deleted.

πŸ”΄ DELETE /changes/{id}/comments/{comment_id} Β  πŸ”‘ change_comments.delete

Hard-deletes the comment.

🟒 GET /changes/{id}/audit Β  πŸ”‘ change_audit.read

Every audited event β€” field changes, status changes, CAB roster edits, votes, comment previews β€” with action (field_change / status_change / cab_vote / comment), old/new values and the analyst, newest first.


πŸ“š Reference data

Under the shared πŸ”‘ reference.read permission:

Endpoint Returns
🟒 GET /change-statuses Draft, Submitted, Pending Approval, Approved, Rejected, Scheduled, In Progress, Completed, Failed, Cancelled β€” with is_closed, is_default, colour
🟒 GET /change-types Standard, Normal, Emergency (plus custom)
🟒 GET /change-priorities Low, Medium, High, Critical
🟒 GET /change-impacts Low, Medium, High
🟒 GET /change-categories Your configured categories (empty until set up)

πŸ§ͺ Worked example: change calendar feed

A read-only key (changes: read) feeds an external calendar or maintenance-window banner:

B="https://your-server/api/v1"; K="Authorization: Bearer fitsm_…"

# Everything scheduled in the next 14 days, soonest first
curl -s -H "$K" "$B/changes?work_start_from=$(date -u +%Y-%m-%dT%H:%M:%SZ)&work_start_to=$(date -u -d '+14 days' +%Y-%m-%dT%H:%M:%SZ)&sort=work_start_at&per_page=100" \
  | jq -r '.data[] | "\(.change_number)  \(.schedule.work_start_at)  [\(.risk.level // "-")]  \(.title)"'

And a pipeline check before deploys β€” is anything risky approved for tonight?

curl -s -H "$K" "$B/changes?status=Approved&risk_level=High" | jq '.meta.total'

Under the hood: REST API β€” How It Works Β· Other modules: Tickets Β· Assets Β· Problems Β· Keys & permissions: System β†’ API (System module) Β· Module docs: Change Management.