/api/projects
Create, read, update, and publish construction projects. Projects own datasheets, standards, files, and members.
GET /api/projectsList 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/:idGet detailed project information including address, vertical, status, and stakeholders
Required permission: project:read
| Name | In | Type | Description |
|---|---|---|---|
| format | query | string | "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`. |
| shape | query | string | "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. |
| draft | query | string | "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. |
| revisionId | query | integer | With 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/revisionsList 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-statusWhat 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
| Name | In | Type | Description |
|---|---|---|---|
| revisionId | query | integer | Specific project revision ID (optional) |
GET /api/projects/:id/datasheetsList 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
| Name | In | Type | Description |
|---|---|---|---|
| revisionId | query | integer | Specific project revision ID (optional) |
| searchQuery | query | string | Optional 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/standardsList standards associated with a project revision, with category, library, and datasheet count.
Required permission: project:read
| Name | In | Type | Description |
|---|---|---|---|
| revisionId | query | integer | Specific project revision ID (optional) |
| searchQuery | query | string | Optional 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-contentGet standard elements with matched datasheet values in a single call. Reduces round-trips for requirement queries.
Required permission: standard:read
| Name | In | Type | Description |
|---|---|---|---|
| standardId | path | integer | The 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). |
| searchQuery | query | string | Keyword filter on element content |
| revisionId | query | integer | Project revision ID for historical data |
| format | query | string | Response format: "verbose" (default, all fields) or "compact" (essential fields only, reduces payload size). Invalid values return 400. |
POST /api/projects/export/:idProduce 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
datasheetIds — arrayisVerbose — booleandatasheetIds: 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/createCreate a new project.
Required permission: project:create
name — stringaddress_state — stringsummary — stringaddress_street — stringaddress_city — stringaddress_postal_code — stringaddress_country — stringlongitude — stringlatitude — stringvertical_id — integerstatus_id — integerowner_company_id — integer company id — OMIT to default to your own companyengineer_id — integer company id — omit unless the user named a real engineer companyprime_contractor_id — integer company id — omit unless the user named a real contractor companysub_contractors — array of integer company ids — omit unless the user named themvertical_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/:idPartial update of project draft. Only fields included in the body are changed — omitted fields are left untouched.
Required permission: project:draft:edit
name — stringsummary — string — REPLACES the stored summary wholesale; it never appends. To add, mention, append, or include something while keeping what is already there, attempt the edit and read the current summary out of the rejection it returns and send the existing text WITH your addition merged into it. Sending only the new sentence deletes everything the summary previously said.address_street — stringaddress_city — stringaddress_state — stringaddress_postal_code — stringaddress_country — string (default: USA)longitude — stringlatitude — stringvertical_id — integerstatus_id — integerowner_company_id — integer company idengineer_id — integer company idprime_contractor_id — integer company idsub_contractors — array of integer company idsconfirm_replace — boolean — set true ONLY when the user explicitly asked you to replace, overwrite, or rewrite the summary. It bypasses the guard that rejects an edit which would discard an existing summary; never set it merely to get past that rejection, and never claim you preserved prior content when you set it.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/createCreate a new draft from the current published revision.
Required permission: project:draft:create
revision_type — stringdraft_name — stringdraft_summary — stringALWAYS 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/publishPublish the current draft as an immutable revision.
Required permission: project:publish
revision_name — stringrevision_type — stringdescription — stringPublishes 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/revertDiscard 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/standardsAdd 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
action — stringstandardId — integercategoryId — integer — REQUIRED for "customize" and "recategorize"; OMIT for a plain "import" (see notes)name — string (REQUIRED when action is "customize")revisionId — integerreplaceOriginal — booleanaction 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/archiveArchive (soft-remove) a standard from the project draft.
Required permission: project:draft:edit
reason — stringREQUIRES 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/restoreRestore an archived standard to the project draft.
Required permission: project:draft:edit
newName — string — rename the standard as it is restored; the ONLY way past a name conflict (see notes)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/:idSoft-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
| Name | In | Type | Description |
|---|---|---|---|
| acknowledgeDistribution | query | string | "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/correctionsIssue a correction notice on a published project revision. Does not mutate the revision or its content anchor.
Required permission: project:correction:issue
project_revision_id — integerseverity — stringtitle — stringdescription — stringrecommended_action — stringrecommended_action_note — stringsuperseded_by_revision_id — integerseverity: 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/withdrawWithdraw a correction (state change, retains the audit row — never a delete).
Required permission: project:correction:withdraw
reason — stringGET /api/projects/:id/correctionsList corrections for a project. Full audit trail (active + withdrawn) by default; ?status=active returns only active banner corrections.
Required permission: project:correction:view
| Name | In | Type | Description |
|---|---|---|---|
| revisionId | query | integer | Scope to one project revision (optional) |
| status | query | string | "active" returns only active corrections; omit for the full audit trail. |
GET /api/projects/:projectId/coordination-checklistDerived 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
| Name | In | Type | Description |
|---|---|---|---|
| revisionId | query | integer | Specific project revision ID (optional) |
| format | query | string | "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/checksCheck off (resolve) a narrative checklist row. Upserts on the unique key, stamping the clause fingerprint from the pinned revision.
Required permission: project:coordination:resolve
standardElementId — integertagHash — stringnote — stringrevisionId — integerREQUIRES 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/:checkIdReverse a narrative check-off. Subject to the same published-revision immutability guard.
Required permission: project:coordination:resolve
GET /api/projects/:id/parametersThe 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
| Name | In | Type | Description |
|---|---|---|---|
| revisionId | query | integer | Specific 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/:nameRecord 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
| Name | In | Type | Description |
|---|---|---|---|
| name | path | string | The 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. |
value — stringunit — stringrevisionId — integerBoth 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-typesList 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-typesList the project status types as { id, name }. Same rule as vertical types: read the id, do not guess it.
GET /api/projects/checksum/:idGet 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
| Name | In | Type | Description |
|---|---|---|---|
| revisionId | query | integer | Specific project revision (defaults to current) |
GET /api/projects/datasheets/:idRead 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
| Name | In | Type | Description |
|---|---|---|---|
| revisionId | query | integer | Specific 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/:idReport 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/deletableReport 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/draftGet 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-auditDeep 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/:revisionCodeGet 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/diffWhat 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
| Name | In | Type | Description |
|---|---|---|---|
| toCode | path | string | Revision code of the NEWER side (may be the open draft) |
| base | query | string | Revision code of the older side (optional; defaults to the nearest published predecessor of :toCode) |
GET /api/projects/:id/standards/archivedList 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-nameCheck whether a standard name is already taken within the project. Ask before customizing or renaming — a colliding name is refused.
Required permission: project:read
| Name | In | Type | Description |
|---|---|---|---|
| name | query | string | Candidate standard name (required) |
GET /api/projects/:projectId/clause-citationResolve 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
| Name | In | Type | Description |
|---|---|---|---|
| standardElementId | query | integer | The clause to resolve |
| revisionId | query | integer | Specific project revision (optional) |
GET /api/projects/:projectId/commentsList 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
| Name | In | Type | Description |
|---|---|---|---|
| revisionId | query | integer | Pin to one project revision |
| anchorType | query | string | 'standard_element' | 'datasheet_field' |
| elementId | query | integer | Filter to one standard element |
| datasheetInstanceId | query | integer | Filter to one datasheet instance |
| intent | query | string | string — 'position' | 'question' | 'suggestion' | 'scope_assertion' |
| state | query | string | Filter by resolution state |
| direction | query | string | string — 'internal' | 'terminate_at_counterpart' | 'bubble_up'; inferred from intent when omitted |
GET /api/projects/:projectId/comments/countsPer-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
| Name | In | Type | Description |
|---|---|---|---|
| revisionId | query | integer | Pin to one project revision |
| anchorType | query | string | 'standard_element' | 'datasheet_field' |
| standardId | query | integer | Filter to one standard |
| datasheetInstanceId | query | integer | Filter to one datasheet instance |
GET /api/projects/:projectId/comments/unforwardedThe 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/rollupThe 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
| Name | In | Type | Description |
|---|---|---|---|
| audience | query | string | Restrict to one audience, e.g. 'bubble_up' |
| intent | query | string | string — 'position' | 'question' | 'suggestion' | 'scope_assertion' |
| state | query | string | Filter by resolution state |
| revisionId | query | integer | Pin to one project revision |
GET /api/projects/:projectId/comments/counterpartsList 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/commentsCreate 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
anchorType — string — 'standard_element' | 'datasheet_field'intent — string — 'position' | 'question' | 'suggestion' | 'scope_assertion'body — string — non-empty, max 10000 charactersstandardId — integerstandardElementId — integerstandardRevisionId — integerdatasheetInstanceId — integerdatasheetElementId — integerprojectRevisionId — integer — the revision the comment is pinned todirection — string — 'internal' | 'terminate_at_counterpart' | 'bubble_up'; inferred from intent when omitteddisposition — string — 'comply' | 'deviation' | 'exception' | 'na'; ONLY valid when intent is 'position'parentCommentId — string — UUID of the comment being replied tocounterpartCompanyId — integercounterpartUserId — integerauthorCompanyId — integer — defaults to the caller's employerscopeCounterpartyCompanyId — integer — for intent 'scope_assertion'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/:idEdit 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
body — string — the replacement textDELETE /api/projects/:projectId/comments/:idDelete your own comment, and only while it is still un-propagated.
Required permission: project:comment
POST /api/projects/:projectId/comments/:id/forwardForward 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
endorsementStance — string — 'relay' | 'endorse' | 'endorse_with_caveat'caveatText — string — required in practice when the stance is 'endorse_with_caveat'targetCompanyId — integertargetUserId — integerOne 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/resolveSet 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
state — string — see notes; must be legal for the comment's intentanswerBody — stringcounterBody — stringLegal 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/inviteInvite 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
email — stringpermission_type — string — 'ProjectViewer' | 'ProjectEditor' | 'ProjectAdmin'; any other value is a 400PATCH /api/projects/:id/archiveArchive 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
reason — string — recorded with the archivePATCH /api/projects/:id/unarchiveReturn an archived project to the active list.
Required permission: project:delete
POST /api/projects/:id/datasheets/:datasheetInstanceId/archiveArchive 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
reason — string — why it is being archivedREQUIRES 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/restoreRestore an archived datasheet to the project's draft.
Required permission: project:draft:edit
DELETE /api/projects/:id/standards/:standardId/archivedPermanently 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/archivedPermanently 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