Standards

/api/standards

Read and edit standards (specifications) — the structured documents that define project requirements. Supports revisioning, working copies, and comments.

Endpoints

GET /api/standards

List standards accessible to the authenticated user. Supports filtering by scope, project, company, library, and category.

Required permission: standard:read

Parameters
NameInTypeDescription
subject_typequerystringFilter by the scope the standard itself belongs to: "global", "company", or "project". To list what a project uses, call GET /api/projects/:id/standards instead: a project mostly adopts global and company standards, so subject_type=project with project_id returns only the few scoped to the project itself.
project_idqueryintegerAlso include the standards scoped to this project (subject_type "project") alongside the global, company and personal standards the list always returns. It does NOT narrow the list to the standards the project uses: for "what standards are in project N", call GET /api/projects/:id/standards.
company_idqueryintegerFilter to standards owned by a specific company
library_idqueryintegerFilter to standards in a specific library
category_idqueryintegerFilter to standards in a specific category
searchquerystringSearch by name or description (ILIKE match)
include_deprecatedquerybooleanInclude deprecated standards (default false)
pagequeryintegerPage number (optional, enables pagination)
per_pagequeryintegerResults per page (1-100, default all)
sortquerystringSort by: "name", "created_at", or "category" (default: category, name)
sort_orderquerystring"asc" or "desc" (default: asc)
formatquerystring"verbose" (default, every field) or "compact", which keeps only identity and navigation fields (id, name, description, scope, library, category, current revision) and drops the lineage/audit metadata. Built for token reduction when listing many standards. 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.

GET /api/standards/:id

Get a standard with its full element content, revision info, and datasheet elements. To LOOK SOMETHING UP (what does a standard require, which element covers X) use GET /search-content first and then read just the element it cites — a full read is for exports, imports, or a complete review of one known standard. WITHHELD CLAUSES: a clause under a moderation hold keeps its address, depth and datasheet fields but returns empty content with a moderation object in its place. That object carries divergence: null, or — when this is a customized copy whose text for the clause already differed from what the notice pointed at before the notice was recorded — {beforeNotice, revisionId, revisionCode, changedAt, userId, userName, noticeRecordedAt}: a record of when this copy changed, never a finding about it, and never a lift of the hold.

Required permission: standard:read

Parameters
NameInTypeDescription
revisionIdqueryintegerSpecific standard revision ID for historical view (optional, defaults to current)
formatquerystringResponse format: "verbose" (default, all fields) or "compact" (essential fields only, reduces payload size). Invalid values return 400. 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. To keep a standard read inside the context budget, fetch the one element you need via GET /api/standards/search-content rather than the whole document.
fields_onlyquerybooleanReturn only the standard's DATASHEET FIELDS — each with its id (what a datasheet create keys elementValues by), address, label, input type, option values and default — and no clause text. Use it before creating or filling out a datasheet: a full read of a large standard is cut to fit, and the cut drops fields. ASSISTANT PATH ONLY — the HTTP route does not read this parameter.

GET /api/standards/:id/revisions

List revision history for a standard

Required permission: standard:view_revisions

GET /api/standards/:id/compare

Compare two revisions of a standard side-by-side. Besides the clause and datasheet changes, returns `tags` ({added, removed}: tag names the newer revision gained and lost) and `contentIdentical` (true when the two revisions have the same content hash, e.g. a revision whose only change is its tags)

Required permission: standard:view_revisions

Parameters
NameInTypeDescription
fromqueryintegerSource revision ID
toqueryintegerTarget revision ID

GET /api/standards/search-content

Hybrid semantic + keyword search over standards CONTENT — the requirement clauses and datasheet fields themselves, not just standard names. USE THIS FIRST for any question about what a standard requires or contains (e.g. "which standard covers corrosion protection?", "where is the sound-level field?"). It returns a ranked list of compact hits — each carrying the owning standard, a CITABLE element address, and a snippet — so you then fetch just that one element by address (e.g. GET /api/standards/:id) to read it in full. Do NOT pull a whole standard or datasheet into context to look something up; full-document reads are for exports or a complete review. Handles vocabulary gaps (e.g. "rust protection" finds "corrosion-resistant coating") and exact jargon (e.g. "NEMA 3R", "208Y/120V") alike. SCOPE CHOICE MATTERS: omit projectId to search the ENTIRE standards library — that is the right scope for "which standard covers X?", "does our library have anything about X?", and anything phrased about the library/company/wiki. Pass projectId ONLY when the question is about what THIS project's own standards say. A project usually pins just a few standards, so a project-scoped miss means "not in this project" — NEVER conclude the library lacks content until an UNSCOPED search also misses. Hits are standards-library content, not project documents — cite them by standard name + element address.

Required permission: standard:read

Parameters
NameInTypeDescription
queryquerystringWhat to search for — a natural-language phrase or exact jargon (required).
projectIdqueryintegerOptional: restrict to the standards pinned to this project's current revision. OMIT for library-wide questions; if a project-scoped search misses, retry without projectId before concluding the content does not exist.
projectRevisionIdqueryintegerOptional, with projectId: the project revision whose pinned standards to search (the one being viewed). Default: the open draft for an author, the issued revision for a reader. Another project's revision, or one you may not read, is a 404; a historical revision needs project:revision:view. 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.
limitqueryintegerOptional: max hits to return (default 20, max 100).

Request body (JSON)

Required

SEMANTIC search: it matches MEANING, so send ONE natural-language phrase describing ONE concept (e.g. "rodent and pest protection for switchgear enclosures"). There are NO boolean operators — a query containing bare OR/AND/NOT, a "|" alternation, or a comma/slash-separated keyword list is REJECTED and no search runs, because such a string embeds as literal words and retrieves worse, not broader. To cover several distinct concepts, make SEPARATE calls, one plain phrase each; never widen or repeat a query to compensate for a miss. REPORTING RESULTS: every hit carries a `confidence` tier — "strong" (real evidence) or "related" (adjacent content, offered without a claim) — and the response reports `strong_count`. When `strong_count` is 0, nothing in the searched content directly addresses the question: say that plainly FIRST. You may then point at the closest "related" hits as WHERE such a requirement would live if it existed (e.g. the enclosure/environment sections of the switchgear standards), explicitly labeled as closest-related context — never presented as findings, never as evidence the requirement exists, and never paraphrased into a requirement the standard does not state. WRONG TOOL FOR BROWSING: this searches requirement TEXT. To find out WHICH standards exist — picking imports for a project, covering several trades or categories, "what do we have on X?" — do NOT run one content search per topic. List the catalog instead: GET /api/standards (optionally with search=, category_id=, library_id=, subject_type=) returns the standards themselves, and GET /api/categories returns the category list to pick from. One listing answers what a dozen content searches only guess at, and each search costs a full turn of your limited budget.

PUT /api/standards/:id

Update a standard. Only standards you own or have company-level update access to can be modified.

Required permission: standard:update

Request body (JSON)

Optional

POST /api/standards/:id/edit

Step 1 of editing a standard: start editing by creating a working copy (returns the existing working copy if one already exists). Then save changes with PUT /:id/working-copy, and save the revision with POST /:id/publish.

Required permission: standard:update

GET /api/standards/:id/working-copy

Get the current working copy (draft) for a standard, with all elements and datasheet elements. Returns 404 if no working copy exists. WITHHELD CLAUSES: if the standard is under a moderation hold, every element is still returned, but a held one arrives SEALED — its content is the single line `<!-- sync:withheld:TOKEN -->`, its toc_content is "Withheld clause", and it carries sealed: true plus seal_token. The identities of the sealed slots are listed in sealedIdentities, and the standard-level notice is in moderation. The original text is never sent. Return a sealed element to PUT /:id/working-copy with its content byte-identical and the stored text is restored server-side; change it and you are authoring a replacement clause. Never invent, reuse or edit a token, and never offer to reconstruct the withheld text. A sealed element’s moderation object carries divergence: null, or — when this is a customized copy whose text for that clause already differed from what the notice pointed at before the notice was recorded — {beforeNotice, revisionId, revisionCode, changedAt, userId, userName, noticeRecordedAt}. That is a record of when this copy changed, never a finding about it: it does not narrow the hold and does not lift it. EDIT VERSION: the response carries `version` (an opaque string — never parse or change it); send it back as baseVersion on PUT /:id/working-copy and POST /:id/publish. Each element carries editedBy ({ userId, displayName } or null) and editedAt: who last changed that clause in this draft, null when unchanged since the draft opened. HTTP callers also get each element's `hash`, the per-clause merge key that PUT /:id/working-copy accepts back in baseElements.

Required permission: standard:view_drafts

PUT /api/standards/:id/working-copy

Two modes. (1) FULL REPLACE (default): send ALL elements — any element not included is deleted. This is not a patch; to add/modify specific elements, first GET the standard to retrieve all elements, modify the array, then PUT the full array back. If the new element count is less than half the existing count, pass confirmReplace: true to proceed. (2) PATCH MODE — for editing datasheet fields only (the common case: changing a field's options/default/drawing_ref without touching the standard's requirement text): pass datasheetElementsOnly: true and OMIT elements entirely. Every requirement element and every datasheet field you do not mention is carried over unchanged automatically — you never need to re-fetch or resubmit the full document. In datasheetElements, each item is matched to an existing field by (element_address, label); to rename/respec a field (change its label), add match_label with the field's CURRENT label so it is matched to the right existing row instead of being added as a new field. To delete a field, list it in the top-level removeDatasheetFields array as { element_address, label } (using its current label, not match_label). WITHHELD CLAUSES: a sealed element returned from GET /:id/working-copy carries `<!-- sync:withheld:TOKEN -->` as its content. Send it back unchanged and the stored text is spliced back in server-side and the clause keeps its identity; send different text and you are authoring a REPLACEMENT — the held clause is retired and a new one is created in its place, recorded against your account. A sealed element you omit entirely is preserved, not deleted, so a hold can never cost a draft its text. A token that does not belong to this working copy is rejected with 409: re-fetch the working copy rather than reusing or editing a token. EDIT CONFLICTS: send baseVersion, the `version` from your most recent read (GET /:id/working-copy or POST /:id/edit) or from your previous save. If someone saved the draft since, the save is refused with 409 and `code: "EDIT_CONFLICT"`; re-read and apply your change again. Every successful save returns the new `version` for the next one.

Required permission: standard:update

Request body (JSON)

Required
Optional

MARKDOWN: an API key or MCP client may send `markdown` instead of `elements`: the whole standard written in SynC markup (syntax: https://synergyinconstruction.com/wiki/about/standards-markup-syntax.md), parsed server-side, full replace, with parse errors returned as 422 with an errors list. A leading YAML frontmatter block is accepted and ignored. This is the easiest way to convert an existing specification. If you are the in-app SynC assistant, markdown is NOT available to this assistant: send `elements`, or `datasheetElementsOnly: true` for a fields-only patch; a markdown-only body is rejected with "Elements array is required". In `elements`, a section heading carries attributes { inToc: true } and a note attributes { isNote: true }; a clause carries neither. DATASHEET FIELD DEFAULTS vs DRAWING REFERENCES: default_value is a literal value pre-filled into the field — for select/radio/checkbox/listbox/combobox it MUST be one of the values in options, or it silently matches nothing ("As indicated on the drawings" is NOT a valid default_value). To make a field defer to the drawings instead of carrying a default, set starting_disposition: 'per_drawings' (this is what actually seeds the "per drawings" state; legacy starts_deferred: true still works) plus drawing_ref: true and optionally drawing_ref_locator, and leave default_value null/omitted — the field can still keep its full options list for when it IS specified later. To make a field start as "Manufacturer's standard" (terminal — the vendor's standard design value governs, no coordination follow-up), set starting_disposition: 'manufacturer_standard'; no drawing_ref needed. constraint: on a range field min, max or exact; on a checkbox or listbox field all_of or any_of; omit it when the direction is not known, never guess. The label then names the quantity without "Minimum" or "Maximum". ADDRESS HIERARCHY (enforced): element addresses are dot-separated numeric outline addresses forming a heading tree — every dotted address requires its parent to be in the submission (submitting '1.1' without '1' is a 400; levels cannot be skipped), addresses must be unique, and top-level sections are plain integers. ATTACHMENT (enforced): every datasheetElements item's element_address must equal the address of an element in the same submission — datasheet fields attach to an existing requirement/heading element; a field pointing at a nonexistent address is a 400, never a silent drop. EDIT CONFLICTS (HTTP): a save whose baseVersion is stale is merged when it carries baseElements — each clause (an element with its datasheet fields) takes whichever side changed it since the base; additions and deletions follow by presence. The merged save returns merged: true, mergedChanges: [{ elementId, address, change: "changed" | "added" | "deleted", editedBy, editedAt }] naming whose changes came in, and workingCopy (the merged draft). Both sides changing the same clause, or a merge that breaks the outline, is 409 EDIT_CONFLICT with metadata { code, reason, currentVersion, editedBy, editedAt, conflicts: [{ elementId, address, baseAddress, theirsAddress, mineAddress, reason, editedBy, editedAt }] }. Nothing is written on a 409. The assistant never merges: its stale save is always a 409.

POST /api/standards/validate-markdown

Check a whole standard written in SynC markup without saving it. Returns 200 with ok, errors (each one would stop PUT /:id/working-copy from saving, worded as that save words it), warnings (the save succeeds, but saving the revision would refuse or flag it), and a preview of what the markup parses into: element counts by kind, each element with its address, kind (section, clause or note) and opening text, and each datasheet field with its type, constraint, default, drawing reference and starting state. Send standardId to also see how a save onto that standard would match its saved revision (matched, new and removed elements) and its open draft (hasOpenDraft; a save needs one, from POST /:id/edit). Run it on every file before saving, and fix every error first.

Required permission: standard:read

Request body (JSON)

Required
Optional

WRITES NOTHING: no draft is opened, changed or saved, so it needs no baseVersion and can be called as often as you like. It parses exactly as PUT /:id/working-copy parses a markdown body, so markup that validates with ok: true saves without parse errors. Element ids are not assigned here: addresses in the preview are outline positions, not ids. errors lists at most 100 messages (errorCount is the total); the preview lists at most 300 elements and 300 fields (truncated: true when cut). Markup longer than 1,000,000 characters is a 413. A standardId you may not read is a 404.

POST /api/standards/:id/publish

Save the current working copy as a new numbered revision (saving a revision) — the final step after POST /:id/edit and PUT /:id/working-copy. The revision is sealed; further edits require opening a new working copy.

Required permissions: standard:update, standard:revision:save

Request body (JSON)

Required
Optional

severity is optional and defaults to minor; accepted values are major|minor|patch (case-insensitive). tag_ids is the revision's topic tag set, and saving the revision is the moment that set becomes the standard's answer to "what is this about?" — the tag filters read the current revision. It REPLACES the set rather than adding to it, so send every tag the revision should end up with. Choose ids from GET /api/standards/tags; only an active tag can be added, and an unknown or retired id is a 400 naming it. ABSENT AND EMPTY ARE NOT THE SAME REQUEST: omit tag_ids to say nothing about tags, which leaves the standard's existing tags in place, and send an empty array ONLY to say the revision should carry no tags at all, which removes every one of them. Anything else that is not an array (null included) is a 400 rather than a guess. If you have no instruction about tags, omit the field.

POST /api/standards

Create a new standard FROM SCRATCH. Standards are reusable assets — before using this, search for an existing wiki/company/personal standard covering the subject (GET /api/standards/search-content or the standards list) and prefer importing or customizing it into the project. Create from scratch only when the user explicitly asks for a brand-new standard or no usable existing standard was found (and say which of those it was).

Required permission: standard:create

Request body (JSON)

Required
Optional

subject_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 standard in someone else's scope.

POST /api/standards/:id/customize

Create a standalone customization. WARNING: Does not add to project draft. Use POST /api/projects/:projectId/standards with action: "customize" instead for project workflows.

Required permission: standard:customize

Request body (JSON)

Required
Optional

Snake_case (category_id, project_id, company_id, source_revision_id) is accepted for each, and is the spelling the validation errors use — an error reading "category_id is required" refers to the categoryId you sent, not a second field. Exactly one of projectId or companyId: sending both is a 400, sending neither is a 400. companyId may be any company where you hold standard:create, not only your primary one; any other is a 403. sourceRevisionId names the revision to copy (default: the current one). It must be a published revision of this standard: another standard's or a missing revision is a 404, a working copy or a revoked wiki edition a 400. A source standard under a moderation notice is refused with 409 and `metadata.code = "STANDARD_UNDER_MODERATION_NOTICE"` — the copy would inherit the notice, so upgrade to the replacement edition named in the error instead, or remove/swap the standard when there is none.

PATCH /api/standards/:id/deprecate

Deprecate a standard (soft removal without deleting). Deprecated standards remain accessible in projects that already use them but are hidden from standard lists by default.

Required permission: standard:delete

Request body (JSON)

Required

PATCH /api/standards/:id/reactivate

Reactivate a deprecated standard. Fails if an active standard with the same name already exists in the same scope.

Required permission: standard:delete

DELETE /api/standards/:id

PERMANENTLY delete a standard, and the SECOND of two steps. Refused with 409 unless the standard has already been deprecated (PATCH /api/standards/:id/deprecate, which is reversible via /reactivate) — destruction is never a single call. Also refused with 409 if the standard is used in any PUBLISHED project revision, and that refusal is terminal: deprecating it first will not unlock this. Customizations derived from it are not deleted; they lose their parent reference.

Required permission: standard:delete

Request body (JSON)

Required

confirmName is MANDATORY and must match the standard's name EXACTLY (character for character) — there is no unconfirmed form of this call. Anything else returns 400 with requiresConfirmation:true plus the real name and the number of customizations that will lose their parent reference. Read the standard first (GET /api/standards/:id) for its exact name, and tell the user that customization count before deleting.

DELETE /api/standards/:id/working-copy

Discard a working copy without saving a revision. WARNING: If the standard has no saved revision yet, this permanently deletes the standard (there is no revision to revert to).

Required permission: standard:update

POST /api/standards/:id/merge

Apply a settled merge plan from GET /:id/merge-plan to a CUSTOMIZED standard. The result is a DRAFT (working copy) of this copy carrying the merged document — NO REVISION IS SAVED; review it and then save the revision with POST /:id/publish. Every conflict in the plan must carry a resolution: there is no default and no suggested side, and a missing one is a 409 with nothing written. The auto-resolved, new and removed lanes apply exactly as the plan listed them. Taking "theirs" keeps the SOURCE clause identity, "mine" and "combine" keep this copy's — never a third one. Refused with 409 when: the plan is stale (planHash mismatch — re-fetch and settle again), this standard already has an unpublished draft open, a clause the merge would bring in from the source is under a moderation notice, a conflict whose clause is withheld in this copy was settled as "combine", or the merge would remove a requirement that datasheet fields are attached to. On success `parent_current_revision_id` advances to the merged revision, so the "parent has been updated" signal stops firing for it.

Required permission: standard:update

Request body (JSON)

Required
Optional

Read GET /:id/merge-plan first and show the user BOTH sides of every conflict plus the auto-resolved lanes — the author is confirming a whole plan, not adjudicating exceptions. Never settle a conflict on their behalf and never describe one side as recommended: the plan deliberately carries no suggestion. A "combine" needs the text they want, not a merge of the two sides invented for them.

POST /api/standards/:id/acknowledge-source

Keep a CUSTOMIZED standard as it is after its source saved a new revision: advances the copy's last-seen source revision WITHOUT merging, so the "source updated" signal stops firing until the source saves another. Writes no content and saves no revision; the acknowledgement is recorded (who, when, which source revision). Refused with 409 when the standard is not a customization, is already current with its source, or the source has saved another revision since toRevisionId (review the newer revision first).

Required permission: standard:update

Request body (JSON)

Required

Only on the user's explicit decision to stay on their current content, after they have seen what the source changed (GET /:id/merge-plan). To take the changes instead, use POST /:id/merge.

POST /api/standards/:id/comments

Add review comment(s) to standard requirement elements. CLOSED — answers 410 Gone to every caller. The legacy comment plane (standard_user_comments) is write-closed: commenting on the standards/wiki side is being replaced by a threaded discussion system, and the project-scoped half moved to project_comments. Existing comments stay READABLE through the GET endpoints and are neither deleted nor migrated. Project correspondence — including a comment on a datasheet field, via anchorType 'datasheet_field' — is authored on POST /api/projects/:projectId/comments.

Required permission: standard:comment

Request body (JSON)

CLOSED — answers 410 Gone to every caller. The legacy comment plane (standard_user_comments) is write-closed: commenting on the standards/wiki side is being replaced by a threaded discussion system, and the project-scoped half moved to project_comments. Existing comments stay READABLE through the GET endpoints and are neither deleted nor migrated. Project correspondence — including a comment on a datasheet field, via anchorType 'datasheet_field' — is authored on POST /api/projects/:projectId/comments.

GET /api/standards/:id/comments

List comments on a standard. Omit element_id for all comments, or filter by specific element.

Required permission: standard:read

Parameters
NameInTypeDescription
element_idqueryintegerFilter to a specific element (optional)
project_idqueryintegerFilter by project (optional)

PUT /api/standards/:id/comments/:commentId/resolve

Resolve or reopen a comment on a standard. CLOSED — answers 410 Gone to every caller. The legacy comment plane (standard_user_comments) is write-closed: commenting on the standards/wiki side is being replaced by a threaded discussion system, and the project-scoped half moved to project_comments. Existing comments stay READABLE through the GET endpoints and are neither deleted nor migrated. Project correspondence — including a comment on a datasheet field, via anchorType 'datasheet_field' — is authored on POST /api/projects/:projectId/comments.

Required permission: standard:comment:resolve

Request body (JSON)

CLOSED — answers 410 Gone to every caller. The legacy comment plane (standard_user_comments) is write-closed: commenting on the standards/wiki side is being replaced by a threaded discussion system, and the project-scoped half moved to project_comments. Existing comments stay READABLE through the GET endpoints and are neither deleted nor migrated. Project correspondence — including a comment on a datasheet field, via anchorType 'datasheet_field' — is authored on POST /api/projects/:projectId/comments.

GET /api/standards/:id/changelog

For a CUSTOMIZED standard, show what the company changed relative to the source revision the copy was last synced to: the source revision of the latest merge on that revision’s own history, or, with no merge, the one it was customized from (`customized_from_revision_id`). An acknowledgement without a merge does not move it. The delta is listed clause by clause and datasheet field by field, matched on element identity so a renumber does not mispair clauses. Each change carries `change_type` (INSERT added, UPDATE modified, DELETE removed, ADDRCHG moved with unchanged text) and, except for an INSERT, `change_details` with the source side (`previous`, and the previous address when it moved). A fork with no edits has no changes. Authorship is per revision, not per clause: every row names who saved the revision and when. Returns nothing meaningful for a standard that is not a customization.

Required permission: standard:view_revisions

Parameters
NameInTypeDescription
revisionIdqueryintegerCompare a specific revision of THIS standard against the source instead of the current one (optional)

GET /api/standards/:id/merge-plan

For a CUSTOMIZED standard, the three-way merge plan against a revision of the standard it was customized from: base (the fork point), theirs (the parent revision) and mine (this copy). Clauses are matched on element identity, never on section number, so a renumber on either side does not mispair them. THIS IS A READ — it computes a plan and changes nothing; POST /:id/merge applies one. `conflicts` are the clauses a person must settle and carry BOTH sides with no suggested answer, each under a stable `conflictId`; `autoResolved`, `newElements` and `removedElements` are the changes that need no decision, listed rather than hidden. Each `autoResolved` entry with resolution "auto_moved" carries a display-only `movedItself` flag: list or show as moved only the entries with `movedItself: true`. One with `movedItself: false` was renumbered only because a parent clause moved; the plan still carries it so the merge moves it with its parent, but it is not a move of its own, and `summary.moved` counts only the true ones. `planHash` names this plan and must be sent back to POST /:id/merge, which refuses a stale one. `baseRevision`, `fromRevision` and `toRevision` each carry their revision `code` beside the id, and `parent` carries `libraryName` — name a revision by its code and a parent by where it lives, never by a database id. A conflict of type "identity_replaced" is the case where the parent corrected a clause at the same address where this copy has its own authored replacement: two different identities at one address, neither preferred. Returns isCustomized false and empty lists for a standard that is not a customization.

Required permission: standard:view_revisions

Parameters
NameInTypeDescription
toRevisionIdqueryintegerMerge from THIS revision of the parent instead of the parent current one (optional).

GET /api/standards/:id/upgrade-status

List the datasheets built on this standard that are out of sync with a given revision of it, with a conflict count each. The batch counterpart of GET /api/datasheet/:id/upgrade-status, and the read to run BEFORE POST /:id/batch-upgrade. `availableRevision` is the revision being measured against.

Required permission: standard:read

Parameters
NameInTypeDescription
projectIdqueryintegerLimit to datasheets in one project (optional)
toStandardRevisionIdqueryintegerMeasure against THIS revision instead of the standard current one. Use it after a project has changed which revision it works to: the datasheets out of sync are the ones that differ from what the PROJECT holds, which may be earlier than the newest. An earlier revision is a legitimate target and reports real work; omit this to keep the default.

GET /api/standards/:id/comments/counts

Comment counts per element for a standard. Subject to the same visibility rule as the comment list: comments from other companies are only counted when they were marked external.

Required permission: standard:read

Parameters
NameInTypeDescription
project_idqueryintegerFilter by project (optional)

GET /api/standards/:id/comment-resolutions

List the available comment-resolution types (the vocabulary a resolution_id comes from). Site-wide reference data — the :id in the path is vestigial and no standard is read.

Required permission: standard:read

POST /api/standards/resolve-references

Batch-resolve `[[library/slug]]` wiki-style cross-references to the standards they point at. A reference that resolves to nothing comes back marked as such rather than omitted, which is how a dangling link is detected.

Required permission: standard:read

Request body (JSON)

Required
Optional

POST /api/standards/errata-banners

Resolve active errata for many standard revisions at once, returning { banners: { [standard_revision_id]: errata[] } }. Public, no auth — announced defects in adopted content are public by design. A revision with no active errata is simply absent from the map.

Request body (JSON)

Required

Non-numeric entries are dropped rather than failing the batch. A non-array is a 400. Library-owner errata (private) are merged in only for a signed-in caller who may read the standard; anonymous callers get wiki errata only.

GET /api/standards/:id/library-errata

Every library-owner erratum on a company-library standard, active and withdrawn, each with issued_at, severity, recommended action, affected revisions and the owning firm; plus permissions.can_issue / can_withdraw for the caller.

Required permission: standard:read

POST /api/standards/:id/library-errata

Issue an erratum on a standard in your firm's library, as the library owner. Only a holder of library:errata:issue in the firm's own scope may issue — reach through site-wide roles does not count. The issue date is stamped by the server and cannot be changed afterwards.

Required permission: library:errata:issue

Request body (JSON)

Required
Optional

POST /api/standards/:id/library-errata/:errataId/withdraw

Withdraw a library-owner erratum. A state, never a delete: the record, its issue date and the reason stay readable to everyone who could read it.

Required permission: library:errata:withdraw

Request body (JSON)

Required

PATCH /api/standards/:id/library-errata/:errataId/correction

Link (or clear, with null) the published revision of the same standard that corrects the erratum.

Required permission: library:errata:issue

Request body (JSON)

Required

POST /api/standards/:id/batch-upgrade

Upgrade many datasheets to a newer revision of this standard in one call, each with its own conflict resolutions. Gated on project:draft:edit, not a standard permission — it writes datasheet revisions into a project's draft.

Required permission: project:draft:edit

Request body (JSON)

Required

Run GET /:id/upgrade-status first to learn which datasheets are behind and GET /api/datasheet/:id/upgrade-plan for each one's conflicts. Valid resolution values: user_accepted (keeps the recorded value — an accept with no newValue is safe, the current value is carried forward; send newValue only to accept a DIFFERENT value), user_modified (writes newValue — a user_modified with no newValue would clear the field and is rejected before it is proposed), user_discarded (drops the value deliberately). Resolve EVERY conflict in the plan and propose it: use the upgrade-plan suggestedValue when one is given, otherwise pick the option that best fits the datasheet as recorded and the standard text. For a definition_changed conflict the suggestedValue is the current value, which still meets the new rules: keep it. Never leave a conflict out and never ask first — the approval card lists each field with the value you chose, and the person reviewing it is the check. Say in the summary which choices were your own reading rather than the plan suggestion. REQUIRES AN OPEN DRAFT: this change lands in the project's current unpublished draft. If the project has no open draft the call is rejected outright — check GET /api/projects/:id/revisions (a revision with is_draft true) and, if there is none, propose POST /api/projects/:id/draft/create first, then make this call. Opening a draft is its own user-approved change; never publish or revert a project to bring a draft into existence.

PUT /api/standards/:id/comments/:commentId

Edit a comment on a standard. CLOSED — answers 410 Gone to every caller. The legacy comment plane (standard_user_comments) is write-closed: commenting on the standards/wiki side is being replaced by a threaded discussion system, and the project-scoped half moved to project_comments. Existing comments stay READABLE through the GET endpoints and are neither deleted nor migrated. Project correspondence — including a comment on a datasheet field, via anchorType 'datasheet_field' — is authored on POST /api/projects/:projectId/comments.

Required permission: standard:comment

Request body (JSON)

CLOSED — answers 410 Gone to every caller. The legacy comment plane (standard_user_comments) is write-closed: commenting on the standards/wiki side is being replaced by a threaded discussion system, and the project-scoped half moved to project_comments. Existing comments stay READABLE through the GET endpoints and are neither deleted nor migrated. Project correspondence — including a comment on a datasheet field, via anchorType 'datasheet_field' — is authored on POST /api/projects/:projectId/comments.

DELETE /api/standards/:id/comments/:commentId

Delete a comment on a standard. CLOSED — answers 410 Gone to every caller. The legacy comment plane (standard_user_comments) is write-closed: commenting on the standards/wiki side is being replaced by a threaded discussion system, and the project-scoped half moved to project_comments. Existing comments stay READABLE through the GET endpoints and are neither deleted nor migrated. Project correspondence — including a comment on a datasheet field, via anchorType 'datasheet_field' — is authored on POST /api/projects/:projectId/comments.

Required permission: standard:comment

GET /api/standards/tags

List the active tag vocabulary (paginated) — the pool a standard's tags are chosen from. Auth only: the tag vocabulary is a global platform taxonomy, not tenant data, and the payload carries labels and an unscoped usage count — no standard titles, ids or project references. Deliberately NOT gated on standard:update so the chip picker works for every authenticated author; attaching a tag still requires standard:update on the revision (PUT /:standardId/revisions/:revId/tags).

Parameters
NameInTypeDescription
limitqueryintegerPage size
offsetqueryintegerPage offset
searchquerystringFilter by label (optional)

POST /api/standards/tags/suggest

Ask the tagging model to suggest tags for standard content. Returns { tags: [{ id, label, confidence }] } — suggestions only; nothing is attached until you PUT the revision tag set. Session only: an API key gets 403, because an API key can't trigger SynC's AI.

Required permission: standard:update

Request body (JSON)

Required
Optional

GET /api/standards/:standardId/revisions/:revId/tags

List the tags attached to one standard revision. Tags live on the REVISION, so a tag set only means anything paired with the revision it was recorded against.

Required permission: standard:read

PUT /api/standards/:standardId/revisions/:revId/tags

Replace the tag set on a standard revision. Only the WORKING COPY may be tagged — a published revision is a 403, because a tag change on a standard is tracked through the normal revision flow rather than written over a record that has already been issued.

Required permission: standard:update

Request body (JSON)

Required

The array REPLACES the tag set rather than adding to it, so send every tag the revision should end up with. An empty array clears all tags. Start a working copy with POST /:id/edit first, then save it as a revision to make the tag change part of a revision.