/api/datasheet
Datasheet instances are project-specific fill-ins of a standard. These endpoints read, revise, and upgrade datasheets.
GET /api/datasheet/:idGet a datasheet instance with its element values, revision info, and comments. Each element carries TWO INDEPENDENT layers, and answering a question about the wrong one is a factual error: (1) the field DEFINITION inherited from the standard — `drawing_ref` (true = the standard says this field's value comes from the drawings), `drawing_ref_locator`, `starting_disposition`, `starts_deferred` (legacy alias), and `default_value`; and (2) this project's INSTANCE STATE — `user_value`, `disposition` (one of "specified", "per_drawings", "not_applicable", "tbd", "manufacturer_standard"), and `disposition_note`. So: "which fields defer to the drawings / are drawing-referenced / should come from the drawings?" is answered by `drawing_ref: true` on the DEFINITION. "Which fields are currently marked per-drawings / left to the drawings on this datasheet?" is answered by `disposition: "per_drawings"` on the INSTANCE. A field very commonly has `drawing_ref: true` while its instance `disposition` is still "specified" (nobody has dispositioned it yet) — that field DOES defer to the drawings. Never report "no fields defer to the drawings" on the strength of the disposition alone; check `drawing_ref` too, and if you mean only one of the two layers, say which one in plain words. `values_summary` ({ total_fields, fields_with_user_value }) tells you how many fields have recorded user values — if `fields_with_user_value` is greater than 0, do not claim fields are unfilled without checking each element's `user_value`.
Required permission: datasheet:read, project:draft:view
| Name | In | Type | Description |
|---|---|---|---|
| id | path | integer | The NUMERIC datasheet instance id (e.g. 95014) — NEVER the datasheet's display name like "DT-1". Get the id from the project's datasheet list (GET /api/projects/:id/datasheets) first. |
| revisionId | query | integer | A specific DATASHEET revision id, to read this datasheet as it stood at that revision (ids from GET /api/datasheet/:id/revisions). Omit for the current state, which is what almost every question wants. ASSISTANT-ONLY: the HTTP route deliberately ignores this parameter and always serves the current revision, so an integration reading historical values must use GET /api/datasheet/:id/compare instead. Historical values are not the datasheet — never answer a question about what the project specifies today out of a revision you pinned, and say which revision you read whenever you pass this. |
| format | query | string | "verbose" (default, all fields) or "compact" (address/label/user_value per element). 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. Note for integrations: the compact shape omits drawing_ref, disposition, and disposition_note, so it cannot answer which fields are deferred. |
| include | query | string | Comma-separated extras to fold into the response: "template" adds the standard's field definitions as they stood for this datasheet's standard revision, and "comments" adds the datasheet's comment thread. Without it neither is returned. 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. ("values" is also accepted but is a no-op — elements always come back.) Prefer include=template over a separate template fetch when you need to know what a field is SUPPOSED to hold, since the instance alone does not carry the definition text. |
GET /api/datasheet/:id/revisionsList revision history for a datasheet instance
Required permission: datasheet:read, project:draft:view
GET /api/datasheet/:id/compareCompare two datasheet revisions side-by-side
Required permission: datasheet:read
| Name | In | Type | Description |
|---|---|---|---|
| from | query | integer | Source revision ID |
| to | query | integer | Target revision ID |
POST /api/datasheetCreate a new datasheet instance for a standard in a project.
Required permission: project:draft:edit
standard_id — integerproject_id — integername — stringscope — stringelementValues — objectREQUIRES 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. Fields omitted from elementValues are filled with the standard's template defaults (stored as real values); a field sent as "" or null is an explicit blank and suppresses its default. Omit fields that should take the default — do not copy defaults into elementValues yourself.
POST /api/datasheet/:id/reviseSave changes and create a new datasheet revision. Requires summary and severity.
Required permission: project:draft:edit
summary — stringchanges — arrayseverity — stringcurrentRevisionId — integer — the datasheet's current_revision_id as YOU last read it; makes this write fail loudly instead of overwriting someone else's edit (see notes)severity is REQUIRED and must be one of MAJOR|MINOR|PATCH (case-insensitive) — it drives the next revision code. Each change must have element_id (from the id field of each element in GET response) and then EITHER a new_value (to specify the field) OR a disposition (to defer it) — see below. DEFERRING A FIELD IS A `disposition`, NEVER AN EMPTY VALUE. To record that a field is left to the drawings, is not applicable, is still to be determined, or is the manufacturer's standard, send disposition: "per_drawings" | "not_applicable" | "tbd" | "manufacturer_standard" (and "specified" to bring a deferred field back to a real value). Sending an empty new_value with NO disposition does NOT defer anything: it wipes the recorded value, leaves the field still marked "specified", and RETURNS SUCCESS — so a revise meant to mark a field per-drawings silently destroys its value and records the opposite of what was asked, with no error to notice. The disposition is the only thing that carries the deferral; the empty value carries nothing. A non-"specified" disposition FORCES the value to null, so never pair one with a new_value expecting both to stick — send the disposition alone. Going the other way (disposition: "specified") needs the new_value in the same change, or the field ends up specified and blank. disposition_note is the free-text "where to look" hint (max 500 characters) and is ALLOWED ONLY alongside a non-"specified" disposition — sending it with a plain value change is a 400. Use it for the drawing/sheet reference when you have one. Pass an empty string as new_value only when the user genuinely wants the recorded value erased with the field left specified. Read the current state first (GET /api/datasheet/:id): each element carries `disposition` and `disposition_note` alongside `user_value`, so you can see which fields are already deferred before proposing a change to them. ALWAYS SEND currentRevisionId, taken from the `current_revision_id` in that same read. It is an optimistic lock: if the datasheet moved on between your read and the approval click — a person editing the same datasheet, another change of your own — the write is rejected with 409 instead of quietly overwriting their work with values computed from a stale copy. Approval is not instant, so this window is real. On a 409, RE-READ the datasheet and reconsider: the other edit may already have done what was asked. Never retry by dropping currentRevisionId to force the write through. This does NOT re-bind the datasheet to a newer standard revision: the new revision inherits the standard revision this datasheet's current revision is already bound to. To move a datasheet onto a newer standard revision use POST /api/datasheet/:id/upgrade (fetch GET /:id/upgrade-plan first) — never describe a revise as an upgrade. 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/datasheet/:idSoft-delete a datasheet instance. Creates a DELETED revision.
Required permission: project:draft:edit
projectRevisionId — integerdeletionReason — stringPOST /api/datasheet/:id/upgradeApply a standard upgrade to this datasheet, resolving element changes. Get the upgrade plan first via GET /:id/upgrade-plan to see conflicts.
Required permission: project:draft:edit
toStandardRevisionId — integerresolutions — arraysummary — stringseverity — stringValid 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). Each resolution needs elementUuid (from upgrade-plan conflicts) or elementId. Every conflict in the plan needs its own resolution entry. severity is optional (MAJOR|MINOR|PATCH); if omitted, the datasheet revision inherits the target standard revision's severity.
POST /api/datasheet/:id/commentsAdd review comment(s) to datasheet fields. Same batch format as standard comments. element_id is the datasheet FIELD id (from the datasheet instance's elements), not a requirement element id. Comments annotate the datasheet; they never change a field value.
Required permission: datasheet:comment
comments — array of { element_id, comment_text, disposition?, visibility? } (recommended batch format)element_id — integer (single comment format)comment_text — string (single comment format)disposition — string (Comply, Deviation, Exception, N/A, Comment — default: Comment)visibility — string (internal or external — default: internal)GET /api/datasheet/:id/commentsList comments on a datasheet instance. Omit standard_element_id for every comment, or filter to one field.
Required permission: datasheet:read
| Name | In | Type | Description |
|---|---|---|---|
| standard_element_id | query | integer | Return only the comments on ONE field, identified by the datasheet FIELD id (the `id` of an element in GET /api/datasheet/:id) — not the printed section number. Omit it for the whole datasheet. Use the filter when the question is about one field: a heavily reviewed datasheet returns a long comment list that can be truncated to fit the context budget, and a truncated list reads exactly like a complete one. |
GET /api/datasheet/:id/upgrade-planGet detailed upgrade plan showing conflicts, auto-resolved elements, and new elements.
Required permission: datasheet:read
| Name | In | Type | Description |
|---|---|---|---|
| toRevisionId | query | integer | Target standard revision ID (defaults to latest) |
POST /api/datasheet/:id/comments/:commentId/resolveResolve or reopen a comment on a datasheet.
Required permission: datasheet:comment
resolution_id — integerreopen — booleanSend resolution_id to resolve. Send { reopen: true } to reopen.
GET /api/datasheet/check-nameCheck whether a datasheet name is already taken for a given project + standard pair. Ask before POST /api/datasheet: names are unique within that pair, so a colliding create is refused.
Required permission: project:read
| Name | In | Type | Description |
|---|---|---|---|
| project_id | query | integer | Project id (required) — also what the permission is scoped to |
| standard_id | query | integer | Standard id (required) |
| name | query | string | Candidate datasheet name (required) |
GET /api/datasheet/:standardId/templateGet a STANDARD's datasheet template — the field definitions a datasheet instance fills in. This is the blank form, not anyone's filled-in copy: the path parameter is a STANDARD id, not a datasheet instance id. Requires BOTH permissions: `standard:read` in the scope that owns the standard (the row question) and `datasheet:read` in any scope (the tier question). Access is their intersection.
Required permission: datasheet:read, standard:read
| Name | In | Type | Description |
|---|---|---|---|
| standardId | path | integer | Standard id (NOT a datasheet instance id) |
GET /api/datasheet/:instanceId/revision/:revisionIdGet one datasheet instance AS OF a specific datasheet revision, with the values that revision recorded. Values are stored mutably and reconstructed by reverse-replaying the change log, so this — not the live read — is what a historical value must be quoted from. A revision that does not belong to the instance is a 404.
Required permission: datasheet:read
| Name | In | Type | Description |
|---|---|---|---|
| instanceId | path | integer | Datasheet instance id |
| revisionId | path | integer | Datasheet revision id belonging to that instance |
GET /api/datasheet/:id/upgrade-statusReport whether a datasheet is behind the current revision of its standard. The cheap first step of the upgrade flow: status → GET /:id/upgrade-plan → POST /:id/upgrade.
Required permission: datasheet:read
GET /api/datasheet/:id/comments/countsComment counts per standard element for a datasheet — how the UI marks which fields have discussion without fetching every comment.
Required permission: datasheet:read
| Name | In | Type | Description |
|---|---|---|---|
| project_id | query | integer | Required for TEMPLATE comments (when :id is a non-numeric id like "template-123"); ignored otherwise. |
POST /api/datasheet/:id/commentCreate ONE comment on a datasheet element. Note the singular path: POST /:id/comments (plural) is the older batch form kept for compatibility. Prefer this one.
Required permission: datasheet:comment
standard_datasheet_element_id — integercomment_text — string — non-emptydisposition — string — 'Comply' | 'Deviation' | 'Exception' | 'N/A' | 'Comment' (default 'Comment')visibility — string — 'internal' | 'external' (default 'internal')visibility 'internal' keeps the comment inside your own company; 'external' exposes it to the other parties on the project. Any value outside the two lists is a 400, not a silent default.
PUT /api/datasheet/:id/comments/:commentIdEdit your OWN comment on a datasheet. Somebody else's comment is refused, not silently ignored.
Required permission: datasheet:comment
comment_text — stringdisposition — stringvisibility — string — 'internal' | 'external'A comment id that does not belong to this datasheet is a 404.
DELETE /api/datasheet/:id/comments/:commentIdDelete your OWN comment on a datasheet. Ownership is enforced server-side.
Required permission: datasheet:comment