REST API¶
Every model has a full REST endpoint under /api/plugins/change-control/:
| Endpoint | Model |
|---|---|
policies/ |
Policy |
policy-rules/ |
PolicyRule |
change-requests/ |
ChangeRequest |
reviews/ |
Review |
checks/ |
MergeCheck |
change-comments/ |
ChangeComment |
They behave like any NetBox endpoint: token authentication, the same filters as the list views, and the same object permissions. Everything below assumes:
Choosing which checks a policy requires¶
A policy's checks is a single list of names, holding both kinds:
- a registered opt-in check, which the plugin or another plugin declared in code;
- a reported check, whose name is yours to invent and which something outside NetBox reports a result for.
The form splits them into two boxes because only the first kind can be offered in a list. The API does not, so send them together:
curl -X PATCH "$NETBOX/api/plugins/change-control/policies/12/" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"checks": ["peer-signoff", "cab-approval"]}'
checks replaces the whole list. To add one without dropping the rest, read it first:
CURRENT=$(curl -s -H "Authorization: Token $TOKEN" \
"$NETBOX/api/plugins/change-control/policies/12/" | jq -c '.checks')
curl -X PATCH "$NETBOX/api/plugins/change-control/policies/12/" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d "{\"checks\": $(jq -cn --argjson c "$CURRENT" '$c + ["cab-approval"]')}"
Find the policies which already require a given check:
curl -s -H "Authorization: Token $TOKEN" \
"$NETBOX/api/plugins/change-control/policies/?required_checks=cab-approval"
See checks which do not apply everywhere for what each kind means.
Reporting a check result¶
This is the half that unblocks a merge. Find the check on the change request, then report on it:
CHECK_ID=$(curl -s -H "Authorization: Token $TOKEN" \
"$NETBOX/api/plugins/change-control/checks/?change_request_id=$CR_ID&name=cab-approval" \
| jq -r '.results[0].id')
curl -X PATCH "$NETBOX/api/plugins/change-control/checks/$CHECK_ID/" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"status": "success", "summary": "Approved at the 14:00 CAB", "details_url": "https://cab.example.com/2026-08-26"}'
status takes pending, running, success, failure, error or skipped. Set running when a job starts, so reviewers can see it is in progress.
A reporter cannot decide whether its own check counts. required is read-only on this endpoint, and the merge gate reads requiredness from the configuration and the policies rather than from the row, so the token your pipeline uses cannot neutralise the gate it reports to. change_request is read-only too: a result belongs to the request it was measured against.
Reading a change request¶
The fields worth acting on:
| Field | Meaning |
|---|---|
status |
draft, needs-review, approved, rejected, completed or abandoned. The people gate only, and read-only: it is derived from the policy evaluation. |
approved |
Whether the policies are satisfied. |
ready_to_merge |
Whether it can actually merge now. Approved is not the same thing: a check or the change window can still block. |
merge_blocked_reason |
Why not, in words, when ready_to_merge is false. |
has_conflicts |
Whether the branch genuinely conflicts with main. |
ref |
Your external reference, for correlating with a ticket or change record. |
branch / branch_name |
The branch, and its name kept after the branch is deleted. |
branch_deleted |
Whether the branch is gone and the record is history only. |
ready_to_merge and has_conflicts are computed at read time on this endpoint, from the policies, the checks, the change window and the branch itself, so neither is what the list filters on. Two cached counterparts are, and they are what the change request list displays:
| Filter | Matches |
|---|---|
gates_cleared |
The policies and every required check were satisfied at the last refresh. Excludes the change window. |
has_conflicts |
The branch conflicted with main at the last refresh. |
Use those to narrow, and read the live fields from the results:
curl -s -H "Authorization: Token $TOKEN" \
"$NETBOX/api/plugins/change-control/change-requests/?status=approved&check_status=failure"
ref filters on a partial, case-insensitive match, and several values are an OR, so a pipeline can look a change request up by the ticket that spawned it:
curl -s -H "Authorization: Token $TOKEN" \
"$NETBOX/api/plugins/change-control/change-requests/?ref=CHG0012345"
Submitting a review¶
curl -X POST "$NETBOX/api/plugins/change-control/reviews/" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"change_request": 42, "decision": "approve", "comment": "Checked the interface descriptions."}'
Note the absent field. reviewer is read-only and always the caller, so a token cannot post an approval attributed to a colleague. Submitting one for somebody else is not an error; it is simply recorded as yours.
decision takes approve, reject or comment. reject is what the interface labels Request changes. Requesting changes needs a comment. A user cannot review their own change request, and a second review by the same user is refused: edit the existing one instead.
Reviews and comments accept Markdown, rendered through NetBox's sanitising filter.
Commenting on one change¶
curl -X POST "$NETBOX/api/plugins/change-control/change-comments/" \
-H "Authorization: Token $TOKEN" -H "Content-Type: application/json" \
-d '{"change_request": 42, "change_diff": 907, "text": "Is this the right rack?"}'
author is read-only and always the caller, for the same reason reviewer is on a review: a comment is part of the record a reviewer reads before approving, so a token must not be able to post one under a colleague's name. Naming somebody else is not an error; the comment is simply recorded as yours.
change_diff has to be a change in the request's own branch. Crossing them is refused with a 400, because such a comment would be invisible on the tab it belongs to and counted as an open thread on a request it does not describe.
Reply within a thread by naming its root comment as parent. A reply must sit on the same change as its parent, and replies are one level deep: a reply to a reply joins the same thread.
Moving a change request through its lifecycle¶
status is read-only, so every transition a person makes is an action rather than a field. See the lifecycle diagram for how they fit together.
curl -X POST "$NETBOX/api/plugins/change-control/change-requests/42/submit/" \
-H "Authorization: Token $TOKEN"
curl -X POST "$NETBOX/api/plugins/change-control/change-requests/42/return-to-draft/" \
-H "Authorization: Token $TOKEN"
curl -X POST "$NETBOX/api/plugins/change-control/change-requests/42/abandon/" \
-H "Authorization: Token $TOKEN"
curl -X POST "$NETBOX/api/plugins/change-control/change-requests/42/reopen/" \
-H "Authorization: Token $TOKEN"
| Action | From | Permission |
|---|---|---|
submit |
Draft | change_changerequest |
return-to-draft |
Needs review, Approved, Rejected | change_changerequest |
abandon |
any open state | abandon_changerequest |
reopen |
Abandoned | reopen_changerequest |
None needs add_changerequest: giving up on a change is not creating one.
Each returns the updated change request. A transition that does not apply, such as abandoning a completed request or submitting one that is already under review, returns 409 with the reason in detail.
Submitting matches the policies and notifies the outstanding reviewers, exactly as the button does.
Events, the other direction¶
To be told when something happens rather than polling, use event rules.