/api/categories
Categories organize standards within a library. Like libraries, categories are scoped to a subject.
GET /api/categoriesList standard categories with optional filtering. Use this to find a valid categoryId before creating a standard or importing/adding an existing standard into a project — a category must belong to the right scope (project-scoped categories only work for that project's own standards).
Required permission: category:read
| Name | In | Type | Description |
|---|---|---|---|
| subject_type | query | string | Scope filter: 'global' | 'company' | 'project' | 'user'. Pair with subject_id for the last three. |
| subject_id | query | integer | The id of that scope — the PROJECT id when subject_type is "project". |
| include_global | query | string | 'true' to also return global categories alongside a non-global scope. |
| tree | query | string | 'true' returns parents with their children nested, instead of one flat list. |
ALWAYS SCOPE THIS CALL WHEN YOU ARE WORKING ON A PROJECT: send subject_type=project and subject_id=<the project id>. The unfiltered call returns every category in every scope you can reach, which is routinely large enough to be truncated to fit the context budget — and what gets dropped is the END of the list, which is where a project's own sub-sections sort. A truncated category list looks exactly like a complete one: you will see plausible categories and no error. If you are about to rename, audit, or report on "all" of a project's sections, an unscoped list CANNOT support that claim, and a `truncated: true` marker in the result means you have not seen them all — re-issue it scoped rather than answering. Add tree=true when the parent/child structure matters, e.g. to reason about sub-sections.
GET /api/categories/:idGet a category by ID with its standard count.
Required permission: category:read
POST /api/categoriesCreate a new standard category.
Required permission: category:create
name — stringsubject_type — stringdescription — stringsubject_id — integerparent_id — integer — the id of the top-level section this one goes UNDER; omit for a top-level sectionsort_order — integer — position among its siblings; omit to appendsubject_id scopes ownership: for subject_type 'user' it MUST be your own user id, for 'company' your own company id. Another account's id is rejected before anything is written, and the rejection names the id to use — do not retry with a guessed id, and never create a category in someone else's scope. parent_id IS HOW A SUB-SECTION IS CREATED. Omit it and the new category is filed at the TOP level, no matter how the user described it — "add a Switchgear sub-section under Electrical" without parent_id silently produces a second top-level section named Switchgear, and the card's "Filed under" row comes back blank. Read the destination id first (GET /api/categories?subject_type=...&subject_id=...&tree=true), and send it. Nesting is ONE LEVEL DEEP: the parent must itself be top-level (a parent that already has a parent is a 400), and it must be in the SAME scope as the category you are creating (same subject_type AND subject_id) — a global parent cannot hold a project section. camelCase (subjectType, subjectId, parentId, sortOrder) is accepted for every field here, but the validation errors name the snake_case spelling — "subject_id is required" is about the subjectId you sent, not a second field. PROJECT-SCOPED CATEGORIES ARE REVISION CONTENT. When the category has subject_type "project", this call requires an open draft and the change is STAGED in it — the live project keeps its old value until the draft is published, so do not report the rename as already in effect, and do not re-issue the call because the live value looks unchanged. With no open draft the call is rejected: propose POST /api/projects/:id/draft/create first (itself a user-approved change), then retry. Categories in the global, company and user scopes are NOT revision content — they need no draft and apply immediately.
PUT /api/categories/:idUpdate a category name, description, parent, or sort order.
Required permission: category:update
name — stringdescription — string — REPLACES the stored description wholesale; it never appends. To add, mention, append, or include something while keeping what is already there, first read the current value (GET /api/categories/:id) and send the existing text WITH your addition merged into it. Sending only the new sentence deletes everything the description previously said.parentId — integer — re-parent this category (null makes it top-level). HTTP CALLERS ONLY — the assistant path does not read this parameter, so sending it here changes nothing about the response and is not a way to alter or shrink a result.sortOrder — integer — position among its siblings. HTTP CALLERS ONLY — the assistant path does not read this parameter, so sending it here changes nothing about the response and is not a way to alter or shrink a result.parentId and sortOrder are HTTP-only: this assistant can rename a section and edit its description, but it cannot re-parent or reorder one — sending either is silently ignored here, so never report a section as moved or reordered on the strength of a successful response. To change where a STANDARD is filed, use POST /api/projects/:id/standards with action "recategorize"; that moves the standard, not the section. PROJECT-SCOPED CATEGORIES ARE REVISION CONTENT. When the category has subject_type "project", this call requires an open draft and the change is STAGED in it — the live project keeps its old value until the draft is published, so do not report the rename as already in effect, and do not re-issue the call because the live value looks unchanged. With no open draft the call is rejected: propose POST /api/projects/:id/draft/create first (itself a user-approved change), then retry. Categories in the global, company and user scopes are NOT revision content — they need no draft and apply immediately.
POST /api/categories/:id/mergeMerge one category (section) into another within the same scope. The source section is emptied and deleted; its standards, placements, and child sub-sections all move to the target.
Required permission: category:update
intoCategoryId — integerDELETE /api/categories/:idPERMANENTLY delete a category (section). Its standards are NOT auto-filed anywhere: name reassignTo and they move there, or confirm by name and they are DELETED along with the category. Sub-categories need an explicit disposition. There is no undo. To empty a section into another one without destroying anything, use POST /api/categories/:id/merge instead.
Required permission: category:delete
reassignTo — integer — category id to move this one's standards into (the safe path)confirmName — string — the category's exact name; REQUIRED instead of reassignTo, and it DELETES the standardschildDisposition — string — reassign | promote | cascade; REQUIRED when the category has sub-categorieschildReassignTo — integer — top-level category id; REQUIRED when childDisposition is "reassign"THE CATEGORY'S OWN STANDARDS. If it holds any, you must choose: reassignTo moves every one of them into that category (validated for scope access) and is the safe path; confirmName — which must match the category name EXACTLY and is only consulted when reassignTo is absent — HARD-DELETES those standards along with their datasheet instances. They are different outcomes, not two ways to confirm the same one. Omit both and the call returns 400 with the real name and standard count, which is the read to quote to the user before choosing. An empty category needs neither. SUB-CATEGORIES. If the category has any, childDisposition is REQUIRED (400 otherwise, listing the children): "reassign" re-parents them under childReassignTo, which must be a TOP-LEVEL category in the SAME scope; "promote" makes them top-level; "cascade" DELETES the child categories, leaving their standards uncategorized (cascade destroys categories, not standards). Any other value is a 400. A project-scoped section is revision content: deleting one requires an open project draft.
POST /api/categories/adoptCopy a category (and its parent, if it has one) from one scope into another — the normal way to bring a global or company section structure into a project. Find-or-create matches on the trimmed, case-insensitive name, so repeating the call returns the same category instead of a duplicate.
Required permission: category:create
sourceCategoryId — integer — the category to copy FROMtargetSubjectType — string — 'project' | 'company' | 'user'targetSubjectId — integer — id of that project/company/userIdempotent by name: an existing category with the same name in the target scope is reused, not duplicated, and neither its description nor its sort order is overwritten. Adopting into the scope the category already lives in is a 400, not a no-op. snake_case spellings (source_category_id, target_subject_type, target_subject_id) are accepted as aliases. Unlike PUT/DELETE on a project-scoped category, this call needs NO open draft — it creates the empty section immediately. What is revision content is what you then file into it.