Projects

/api/projects

Create, read, update, and publish construction projects. Projects own datasheets, standards, files, and members.

Endpoints

GET /api/projects

List projects accessible to the authenticated user, organized by access level. GET /api/projects/my-projects is an alias for this same call — one endpoint, two paths.

Required permission: project:read

GET /api/projects/details/:id

Get detailed project information including address, vertical, status, and stakeholders

Required permission: project:read

Parameters
NameInTypeDescription
formatquerystring"verbose" (default, all fields) or "compact" (id/name/vertical/status/revision code/city/state). 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. The assistant already receives a reduced projection of about a dozen fields, and unlike compact it keeps `summary`.
shapequerystring"v2" returns the consolidated, audience-gated project view (header, standards, datasheets and members in one payload) instead of the flat project-header lookup this endpoint otherwise serves. Recipients see the audience-filtered form of that payload. 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.
draftquerystring"1" or "true" assembles the project's working DRAFT through the v2 renderer. Only meaningful with shape=v2, and AUTHOR-ONLY: a caller without project:draft:edit gets 403, never a filtered payload, because a draft is never visible to recipients. 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.
revisionIdqueryintegerWith shape=v2, render a specific published revision instead of the newest one you may read. A revision your access does not reach is refused rather than downgraded. Ignored without shape=v2. 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/projects/:id/revisions

List project revisions. Includes draft revisions if you have project:draft:view permission. Each revision has is_draft field.

Required permission: project:revision:view, project:draft:view

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

What in this project is BEHIND its parent — the one read that answers "have any of our standards been updated?". Reports three INDEPENDENT layers, which are not the same question: datasheetsBehind (a datasheet behind the revision the PROJECT PINS for its standard — the only layer that blocks publishing), standardsBehind (the project pins an older revision than the repository has — advisory, never blocks; upgrading is a deliberate step), and repositoryBehindWiki (the standard was adopted from an older wiki EDITION than the wiki has since adopted — this is the layer a user means by "has the wiki updated this?"). Also returns `checked` ({standards, datasheets}) as the denominator and `upToDate`. Defaults to the draft revision if there is one, otherwise the current published revision.

Required permission: project:read

Parameters
NameInTypeDescription
revisionIdqueryintegerSpecific project revision ID (optional)

GET /api/projects/:id/datasheets

List all datasheets for a project revision, each with its `upgrade_status` (current | needs_upgrade | no_revision). `needs_upgrade` means a newer standard revision EXISTS in the repository — advisory only; it does not block publishing. For the project-wide answer across standards AND datasheets, use GET /:id/upgrade-status instead. Defaults to draft revision if available, otherwise current published.

Required permission: project:read

Parameters
NameInTypeDescription
revisionIdqueryintegerSpecific project revision ID (optional)
searchQueryquerystringOptional case-insensitive substring filter on the datasheet NAME only (an ILIKE match — it does NOT search requirement text, values, or descriptions, so send a name fragment like "MCC-2", never a question or a requirement phrase). USE IT whenever you are after specific datasheets rather than the whole list: an unfiltered list on a large project can exceed the context budget and be TRUNCATED, and what you receive then looks exactly like a complete list. A "truncated: true" marker in the result means you have not seen everything — re-issue with this filter rather than answering from what came back. To search what the standards actually REQUIRE (not what they are called), use GET /api/standards/search-content instead.

GET /api/projects/:id/standards

List standards associated with a project revision, with category, library, and datasheet count.

Required permission: project:read

Parameters
NameInTypeDescription
revisionIdqueryintegerSpecific project revision ID (optional)
searchQueryquerystringOptional case-insensitive substring filter on the standard NAME only (an ILIKE match — it does NOT search requirement text, values, or descriptions, so send a name fragment like "switchgear", never a question or a requirement phrase). USE IT whenever you are after specific standards rather than the whole list: an unfiltered list on a large project can exceed the context budget and be TRUNCATED, and what you receive then looks exactly like a complete list. A "truncated: true" marker in the result means you have not seen everything — re-issue with this filter rather than answering from what came back. To search what the standards actually REQUIRE (not what they are called), use GET /api/standards/search-content instead.

GET /api/projects/:projectId/standards/:standardId/integrated-content

Get standard elements with matched datasheet values in a single call. Reduces round-trips for requirement queries.

Required permission: standard:read

Parameters
NameInTypeDescription
standardIdpathintegerThe STANDARD's numeric id (from the project standards list) — NOT a datasheet instance id. For per-field datasheet flags like drawing_ref, read the datasheet instance instead (GET /api/datasheet/:id).
searchQueryquerystringKeyword filter on element content
revisionIdqueryintegerProject revision ID for historical data
formatquerystringResponse format: "verbose" (default, all fields) or "compact" (essential fields only, reduces payload size). Invalid values return 400.

POST /api/projects/export/:id

Produce the full JSON export of a project revision (the same payload the UI download button produces): project header, the revision’s section list (heading path + scope statement, in document order), and every datasheet with its recorded element values and the section it is filed under. The payload is LARGE — read it to answer questions or summarize it, never paste it back wholesale. To actually SEND an export to a third party (manufacturer, contractor), tell the user to use the Export button on the project page: the download is the delivery mechanism, and this action cannot attach or transmit a file.

Required permission: project:export

Request body (JSON)

Optional

datasheetIds: array of datasheet instance ids to limit the export to (omit or empty = every datasheet). isVerbose: false/omitted (default) exports the chosen values only — compact; true additionally emits every template element with its options, default_value, and input_type, which is much larger. Verbosity does NOT change the section list — both forms carry it in full. Exports the current published revision (or the draft when the project has never been published); it takes no revisionId. Every call is recorded in the project export audit log. When the revision is published, the file also carries certificate/signature/content — a SynC-signed content certificate and the canonical payload it attests to, which makes the file verifiable offline. Those three keys are additive; the project/datasheets schema is unchanged.

POST /api/projects/create

Create a new project.

Required permission: project:create

Request body (JSON)

Required
Optional

vertical_id must be one of these project_vertical_types ids, taken verbatim from this list — never guessed or inferred: 1 residential, 2 industrial, 3 water treatment, 4 data center, 5 medical, 6 commercial, 7 mining, 8 oil and gas, 9 agriculture, 10 renewable, 11 marine, 12 aerospace, 13 food and beverage, 14 institutional. The same list is served by GET /api/projects/vertical-types. An id outside 1-14 is rejected. Water, wastewater, and treatment-plant projects are 3 (water treatment) — NOT 4. If none of the 14 fits, omit vertical_id rather than picking the closest number. The response echoes the RESOLVED vertical name that was actually applied; report that back, not what you intended. Stakeholder fields (owner_company_id, engineer_id, prime_contractor_id, sub_contractors) take plain numeric company ids of REAL companies the user named — never guess or invent an id. An omitted owner_company_id defaults to the caller's own company; the result echoes the applied owner_company name.

PUT /api/projects/edit/:id

Partial update of project draft. Only fields included in the body are changed — omitted fields are left untouched.

Required permission: project:draft:edit

Request body (JSON)

Optional

vertical_id must be one of these project_vertical_types ids, taken verbatim from this list — never guessed or inferred: 1 residential, 2 industrial, 3 water treatment, 4 data center, 5 medical, 6 commercial, 7 mining, 8 oil and gas, 9 agriculture, 10 renewable, 11 marine, 12 aerospace, 13 food and beverage, 14 institutional. The same list is served by GET /api/projects/vertical-types. An id outside 1-14 is rejected. Water, wastewater, and treatment-plant projects are 3 (water treatment) — NOT 4. If none of the 14 fits, omit vertical_id rather than picking the closest number. The response echoes the RESOLVED vertical name that was actually applied; report that back, not what you intended. Stakeholder fields take plain numeric company ids of REAL companies the user named — never guess or invent an id. 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.

POST /api/projects/:id/draft/create

Create a new draft from the current published revision.

Required permission: project:draft:create

Request body (JSON)

Optional

ALWAYS send draft_name and draft_summary, in the words of whoever asked for the work. The draft name is how the revision is identified in the project history for good, and the summary is the record of WHY it was opened — a draft with neither is indistinguishable from every other one later. Both are still editable at publish time (revision_name / description on the publish call), so a reasonable name now costs nothing. Do not invent a scope the user did not ask for: name the draft after the change requested, not after what you plan to do beyond it.

POST /api/projects/:id/publish

Publish the current draft as an immutable revision.

Required permission: project:publish

Request body (JSON)

Required
Optional

Publishes the project's OPEN DRAFT, so the project must have one — with no open draft there is nothing to publish and the call is rejected. Publishing is irreversible: it freezes the draft as an immutable revision. Only publish when the user explicitly asked for it; never as a step toward some other goal.

POST /api/projects/:id/revert

Discard the current draft and revert to the last published revision. Requires a published revision to fall back to — a project whose draft is its ONLY revision cannot be reverted (edit or publish the draft instead).

Required permission: project:draft:delete

POST /api/projects/:id/standards

Add a standard to the project draft (import by reference, or customize it into a project-scoped copy), or move a standard already in the draft to a different project section.

Required permission: project:update

Request body (JSON)

Required
Optional

action is one of three: "import" (link an existing standard — THE DEFAULT; "add standard X to the project" always means import), "customize" (create a NEW project-scoped editable copy — ONLY when the user explicitly asks to modify the standard's content for this project; by default it also REPLACES the original in the draft — replaceOriginal: false keeps both), or "recategorize" (MOVE a standard that is ALREADY in the draft from one project section to another — this is the action for "move X into section Y" / "refile X"; it adds nothing and removes nothing, so never archive-and-re-import to reorganize). Any other value is a 400. A customize with no follow-up content edit is always wrong — use import. A standard that is already project-scoped to this project is added with import too, never re-customized. categoryId: OMIT IT ON A PLAIN IMPORT. Left out, the import auto-files the standard into the project section that matches the standard's own category, creating that section only if the project does not already have it — which is the filing the user expects. Naming a categoryId on an import overrides that and drops the standard wherever you pointed; CREATING a category first just to have an id to send manufactures a section nobody asked for and is the wrong move. categoryId IS required for action "customize" (rejected without one) and for "recategorize" (it is the destination section). When you do send it, it must be a project-scoped category of THIS project — a global or company category is rejected — so read the real ones from GET /api/categories?subject_type=project&subject_id=<this project id>. Before adding, check this project's current standards (GET /:id/standards) — if the standard (or a project copy of it) is already there, do not add anything: create the datasheet on the existing one, edit its working copy, or recategorize it if it is only in the wrong section. If the call fails with "a standard with that name already exists", a copy already exists — do NOT retry with a different name (that creates a duplicate); look up the existing one and use it, or ask the user how to proceed. 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.

POST /api/projects/:id/standards/:standardId/archive

Archive (soft-remove) a standard from the project draft.

Required permission: project:draft:edit

Request body (JSON)

Required

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.

POST /api/projects/:id/standards/:standardId/restore

Restore an archived standard to the project draft.

Required permission: project:draft:edit

Request body (JSON)

Optional

newName RENAMES the standard as part of the restore, and it is the only escape from the one error this call really returns: if an ACTIVE standard in the draft already holds the archived one's name, the restore fails with 409 "a standard with the name X already exists". That is a genuine conflict, not a transient failure — retrying the same call returns the same 409 forever. Do not archive the active standard to clear the way, and do not give up: either restore under a newName that distinguishes the two (say which edition/revision it is, in the words of whoever asked), or tell the user the active standard already covers it and ask which they want. Restoring also brings back any datasheets archived alongside the standard. 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.

DELETE /api/projects/:id

Soft-delete a project. A project that has been shared with anyone returns 409 until the caller acknowledges the consequences — see acknowledgeDistribution.

Required permission: project:delete

Parameters
NameInTypeDescription
acknowledgeDistributionquerystring"true" proceeds with deleting a project that has been distributed. Without it the request returns 409 listing who received the project — this is deliberate, not a bug, and the 409 body is what the recipient-facing consequences look like. Only send it after a human has been shown that list and said to go ahead. Accepted as a body boolean too (acknowledgeDistribution: true). A project nobody received deletes without it.

POST /api/projects/:id/corrections

Issue a correction notice on a published project revision. Does not mutate the revision or its content anchor.

Required permission: project:correction:issue

Request body (JSON)

Required
Optional

severity: minor|major|critical. recommended_action: use_corrected_revision|discard_section|await_correction|consult_other_source|clarification_only|do_not_use|verify_independently|other. 'other' requires recommended_action_note.

POST /api/projects/:id/corrections/:cid/withdraw

Withdraw a correction (state change, retains the audit row — never a delete).

Required permission: project:correction:withdraw

Request body (JSON)

Required

GET /api/projects/:id/corrections

List corrections for a project. Full audit trail (active + withdrawn) by default; ?status=active returns only active banner corrections.

Required permission: project:correction:view

Parameters
NameInTypeDescription
revisionIdqueryintegerScope to one project revision (optional)
statusquerystring"active" returns only active corrections; omit for the full audit trail.

GET /api/projects/:projectId/coordination-checklist

Derived drawing-coordination checklist for a project revision. Defaults to the draft revision if present, otherwise the current published revision. The register is an authorship-side QC work product — recipients of a shared project cannot read it.

Required permission: project:coordination:view

Parameters
NameInTypeDescription
revisionIdqueryintegerSpecific project revision ID (optional)
formatquerystring"csv" returns a text/csv attachment instead of JSON. 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. An attachment has no meaning as a tool result, so the assistant always receives the JSON rows — never offer the user a CSV download from this action.

POST /api/projects/:projectId/coordination-checklist/checks

Check off (resolve) a narrative checklist row. Upserts on the unique key, stamping the clause fingerprint from the pinned revision.

Required permission: project:coordination:resolve

Request body (JSON)

Required
Optional

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.

DELETE /api/projects/:projectId/coordination-checklist/checks/:checkId

Reverse a narrative check-off. Subject to the same published-revision immutability guard.

Required permission: project:coordination:resolve

GET /api/projects/:id/parameters

The project's parameter register for one revision: every parameter name the standards on the revision assert (via [[parameter: …]] in clause prose or in a derived datasheet field's basis), UNION every name the revision has answered. Unanswered names are INCLUDED with a null value — they are the point of the register, and each one prints as "TBD" in the issued document. An answered name whose last asserting standard has left the draft is shown with asserted:false and will be dropped when the revision is published. A published revision serves the description SNAPSHOT frozen at publish, a draft serves the library's current description; descriptionSource says which.

Required permission: project:read

Parameters
NameInTypeDescription
revisionIdqueryintegerSpecific project revision ID (optional). Omitted means the newest revision the caller may read — the open draft for an author, the newest published revision for a share recipient.

PUT /api/projects/:id/parameters/:name

Record this project's answer to one parameter on its DRAFT revision. An upsert: a second answer for the same name on the same revision REPLACES the first — a parameter is answered exactly once per revision, and there is no answer history to choose between. Answering a parameter on a PUBLISHED revision is rejected with a 409; published revisions are immutable, so the change has to go on a draft and be published forward. The value and unit are free text and are never validated, converted or unit-checked: an answer is engineering input, and rejecting it because a parser did not recognise it is the failure this design avoids. Sending value: null clears the answer without deleting the row.

Required permission: project:draft:edit

Parameters
NameInTypeDescription
namepathstringThe parameter name, URL-encoded. Matched case-insensitively with whitespace collapsed, the same way the scanner normalises it — "Site Ambient" and "site ambient" address the same parameter. The name is DERIVED from the asserting standards and cannot be invented here to mean something new.

Request body (JSON)

Optional

Both value and unit REPLACE what is stored — read GET /api/projects/:id/parameters first if you mean to amend rather than overwrite. revisionId is optional and defaults to the open draft, which is the only revision this endpoint can write. 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.

GET /api/projects/vertical-types

List the project vertical types as { id, name }. READ THE ID FROM HERE — a vertical_id is never inferred from a number or a name resemblance.

GET /api/projects/status-types

List the project status types as { id, name }. Same rule as vertical types: read the id, do not guess it.

GET /api/projects/checksum/:id

Get the content anchor of a project revision: its SHA-256 content_hash, the short fingerprint (first 12 hex characters), the hash recipe, and the opaque verify_id. A LOOKUP of what was recorded at publication — it does not re-derive anything, so it proves what the anchor IS, not that the content still matches it. For that, use GET /:id/integrity-audit.

Required permission: project:read

Parameters
NameInTypeDescription
revisionIdqueryintegerSpecific project revision (defaults to current)

GET /api/projects/datasheets/:id

Read a project's datasheets both flat and grouped by discipline and specification — the shape the project view renders from. Audience-gated: a recipient does not receive the author-only upgrade signals. Prefer GET /:id/datasheets for a plain list.

Required permission: project:read, project:revision:view

Parameters
NameInTypeDescription
revisionIdqueryintegerSpecific project revision. A HISTORICAL revision additionally requires project:revision:view — it is refused with 403, never quietly served as the current one.

GET /api/projects/edit-access/:id

Report whether the caller can enter edit mode on a project and whether a draft already exists — the check to run before offering an edit, so the answer is not "start a draft" for someone who cannot.

Required permission: project:read

GET /api/projects/:id/deletable

Report whether a project can be deleted, and what blocks it if not. DELETE /:id runs the same check and refuses — this is the way to find out first.

Required permission: project:delete

GET /api/projects/:id/draft

Get the project's open DRAFT revision, or nothing if there is none. The read between POST /:id/draft/create and POST /:id/publish — a draft is never visible to recipients.

Required permission: project:draft:view

GET /api/projects/:id/integrity-audit

Deep tamper check of the project's CURRENT PUBLISHED revision. Unlike the checksum lookup, this RE-DERIVES the hash from the stored content, recursively recomputing each child standard and datasheet first (the composite folds child anchors, so a child-level change would otherwise be invisible), and reports whether it still matches the published anchor. Returns a verdict, counts, both fingerprints, and the TYPE and ID of any mismatch — never element content. A project with no published revision answers status "no_published_revision" with ok:null, which is not a failure.

Required permission: project:read

GET /api/projects/:id/revisions/:revisionCode

Get one project revision by its REVISION CODE (e.g. "2.1.0"), not by numeric id — the identifier people cite.

Required permission: project:revision:view

GET /api/projects/:id/revisions/:toCode/diff

What CHANGED between two project revisions: standards added/removed/re-pinned, datasheets added/removed/revised (with the field-level before/after), files added/removed, header field changes, and section headings renamed/re-parented/reordered. :toCode is the NEWER side; the older side comes from ?base=<code> and defaults to :toCode's nearest published predecessor. Any two revisions can be compared, not just adjacent ones. THE OPEN DRAFT IS DIFFABLE by its author — pass the draft's code as :toCode to see what publishing would issue. This is the ONLY way to answer "what changed"; do NOT try to infer it by comparing content hashes, because a draft carries its parent revision's anchor until it is published.

Required permission: project:revision:view

Parameters
NameInTypeDescription
toCodepathstringRevision code of the NEWER side (may be the open draft)
basequerystringRevision code of the older side (optional; defaults to the nearest published predecessor of :toCode)

GET /api/projects/:id/standards/archived

List standards archived out of the project's draft — what POST /:id/standards/:standardId/restore can bring back. They do not appear in the normal standards list.

Required permission: project:read

GET /api/projects/:id/standards/check-name

Check whether a standard name is already taken within the project. Ask before customizing or renaming — a colliding name is refused.

Required permission: project:read

Parameters
NameInTypeDescription
namequerystringCandidate standard name (required)

GET /api/projects/:projectId/clause-citation

Resolve ONE standard clause plus its parent and child clauses, as pinned by the project revision — the citation read behind coordination and comment workflows. Gated on project:read alone: it is a reading affordance, not an AI feature.

Required permission: project:read

Parameters
NameInTypeDescription
standardElementIdqueryintegerThe clause to resolve
revisionIdqueryintegerSpecific project revision (optional)

GET /api/projects/:projectId/comments

List the project comments VISIBLE TO THE CALLER. Visibility is path membership plus current project read access — an empty result means none are addressed to you, not that the project has none.

Required permission: project:read

Parameters
NameInTypeDescription
revisionIdqueryintegerPin to one project revision
anchorTypequerystring'standard_element' | 'datasheet_field'
elementIdqueryintegerFilter to one standard element
datasheetInstanceIdqueryintegerFilter to one datasheet instance
intentquerystringstring — 'position' | 'question' | 'suggestion' | 'scope_assertion'
statequerystringFilter by resolution state
directionquerystringstring — 'internal' | 'terminate_at_counterpart' | 'bubble_up'; inferred from intent when omitted

GET /api/projects/:projectId/comments/counts

Per-element comment counts for the badges: { commentCounts: { [standardElementId]: n } }. Counted through the SAME visibility predicate as the list, so a count never reveals a comment the caller could not read. Datasheet-anchored comments are keyed by their standard element id, matching the standard-element case.

Required permission: project:read

Parameters
NameInTypeDescription
revisionIdqueryintegerPin to one project revision
anchorTypequerystring'standard_element' | 'datasheet_field'
standardIdqueryintegerFilter to one standard
datasheetInstanceIdqueryintegerFilter to one datasheet instance

GET /api/projects/:projectId/comments/unforwarded

The caller's duty-to-bubble-up checklist: bubble_up comments they have received, not yet forwarded onward, and not terminally resolved. Each row is an open obligation.

Required permission: project:read

GET /api/projects/:projectId/comments/rollup

The as-bid rollup: citation-anchored comments visible to the caller, with the provenance trail of who forwarded what and the latest resolution state of each.

Required permission: project:read

Parameters
NameInTypeDescription
audiencequerystringRestrict to one audience, e.g. 'bubble_up'
intentquerystringstring — 'position' | 'question' | 'suggestion' | 'scope_assertion'
statequerystringFilter by resolution state
revisionIdqueryintegerPin to one project revision

GET /api/projects/:projectId/comments/counterparts

List the parties the caller may address a comment to, taken from the live share graph — upstream (who shared to them) and downstream (who they shared to). Read this before composing: a counterpart not on this list is not addressable. Gated on project:comment, because authoring is the only reason to need it.

Required permission: project:comment

POST /api/projects/:projectId/comments

Create a project comment anchored to a standard clause or a datasheet field, addressed to one counterpart. The author silo defaults to the caller's employer company.

Required permission: project:comment

Request body (JSON)

Required
Optional

Set EXACTLY ONE anchor: standardElementId for a standard_element, datasheetElementId (with datasheetInstanceId) for a datasheet_field. Omitting direction infers it from intent. A disposition on any intent other than 'position' is a 400, not an ignored field. Counterparts come from GET /:projectId/comments/counterparts; both counterpart fields null makes it a no-chain comment nobody else sees.

PATCH /api/projects/:projectId/comments/:id

Edit the BODY of your own comment, and only while it is still un-propagated. Once it has been forwarded it is part of somebody else's record and can no longer be changed.

Required permission: project:comment

Request body (JSON)

Required

DELETE /api/projects/:projectId/comments/:id

Delete your own comment, and only while it is still un-propagated.

Required permission: project:comment

POST /api/projects/:projectId/comments/:id/forward

Forward a comment ONE HOP to your immediate upstream counterpart. You must say how you are passing it on — relaying it, endorsing it, or endorsing it with a caveat — because that stance becomes part of the record the next party reads.

Required permission: project:comment:forward

Request body (JSON)

Required
Optional

One hop only — forwarding does not broadcast, and it cannot widen who can see the comment beyond the next party.

POST /api/projects/:projectId/comments/:id/resolve

Set the resolution state of a comment. Ball-in-court is enforced: only the party the comment is currently with may resolve it, and the allowed states depend on the intent.

Required permission: project:comment:resolve

Request body (JSON)

Required
Optional

Legal states by intent — position: accepted, accepted_as_noted, revise_resubmit, rejected, counter; question: answered, closed; suggestion: considered, adopted, declined; scope_assertion: acknowledged, disputed. A state outside its intent is a 400.

POST /api/projects/:projectId/invite

Invite somebody to a project at a given role, by email. An existing account is granted access directly; an unknown email creates a pending invitation. The grant is bound to the email it was issued to.

Required permission: project:update

Request body (JSON)

Required

PATCH /api/projects/:id/archive

Archive a whole project — hidden from the active list, not deleted. Gated on project:delete, because it is the reversible half of the same authority.

Required permission: project:delete

Request body (JSON)

Optional

PATCH /api/projects/:id/unarchive

Return an archived project to the active list.

Required permission: project:delete

POST /api/projects/:id/datasheets/:datasheetInstanceId/archive

Archive a datasheet out of the project's DRAFT. The mirror of the standards archive endpoint, and reversible via the restore route.

Required permission: project:draft:edit

Request body (JSON)

Required

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.

POST /api/projects/:id/datasheets/:datasheetInstanceId/restore

Restore an archived datasheet to the project's draft.

Required permission: project:draft:edit

DELETE /api/projects/:id/standards/:standardId/archived

Permanently remove an ALREADY-ARCHIVED standard from the project's draft. Only works on archived standards — archive it first. There is no undo, and published revisions that contain it are untouched.

Required permission: project:draft:edit

DELETE /api/projects/:id/datasheets/:datasheetInstanceId/archived

Permanently remove an ALREADY-ARCHIVED datasheet from the project's draft. Same rules as the standards equivalent: archived-only, no undo, published revisions unaffected.

Required permission: project:draft:edit