Datasheets

/api/datasheet

Datasheet instances are project-specific fill-ins of a standard. These endpoints read, revise, and upgrade datasheets.

Endpoints

GET /api/datasheet/:id

Get 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

Parameters
NameInTypeDescription
idpathintegerThe 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.
revisionIdqueryintegerA 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.
formatquerystring"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.
includequerystringComma-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/revisions

List revision history for a datasheet instance

Required permission: datasheet:read, project:draft:view

GET /api/datasheet/:id/compare

Compare two datasheet revisions side-by-side

Required permission: datasheet:read

Parameters
NameInTypeDescription
fromqueryintegerSource revision ID
toqueryintegerTarget revision ID

POST /api/datasheet

Create a new datasheet instance for a standard in a project.

Required permission: project:draft:edit

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. 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/revise

Save changes and create a new datasheet revision. Requires summary and severity.

Required permission: project:draft:edit

Request body (JSON)

Required
Optional

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/:id

Soft-delete a datasheet instance. Creates a DELETED revision.

Required permission: project:draft:edit

Request body (JSON)

Required

POST /api/datasheet/:id/upgrade

Apply 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

Request body (JSON)

Required
Optional

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). 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/comments

Add 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

Request body (JSON)

Optional

GET /api/datasheet/:id/comments

List comments on a datasheet instance. Omit standard_element_id for every comment, or filter to one field.

Required permission: datasheet:read

Parameters
NameInTypeDescription
standard_element_idqueryintegerReturn 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-plan

Get detailed upgrade plan showing conflicts, auto-resolved elements, and new elements.

Required permission: datasheet:read

Parameters
NameInTypeDescription
toRevisionIdqueryintegerTarget standard revision ID (defaults to latest)

POST /api/datasheet/:id/comments/:commentId/resolve

Resolve or reopen a comment on a datasheet.

Required permission: datasheet:comment

Request body (JSON)

Optional

Send resolution_id to resolve. Send { reopen: true } to reopen.

GET /api/datasheet/check-name

Check 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

Parameters
NameInTypeDescription
project_idqueryintegerProject id (required) — also what the permission is scoped to
standard_idqueryintegerStandard id (required)
namequerystringCandidate datasheet name (required)

GET /api/datasheet/:standardId/template

Get 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

Parameters
NameInTypeDescription
standardIdpathintegerStandard id (NOT a datasheet instance id)

GET /api/datasheet/:instanceId/revision/:revisionId

Get 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

Parameters
NameInTypeDescription
instanceIdpathintegerDatasheet instance id
revisionIdpathintegerDatasheet revision id belonging to that instance

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

Report 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/counts

Comment counts per standard element for a datasheet — how the UI marks which fields have discussion without fetching every comment.

Required permission: datasheet:read

Parameters
NameInTypeDescription
project_idqueryintegerRequired for TEMPLATE comments (when :id is a non-numeric id like "template-123"); ignored otherwise.

POST /api/datasheet/:id/comment

Create 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

Request body (JSON)

Required
Optional

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/:commentId

Edit your OWN comment on a datasheet. Somebody else's comment is refused, not silently ignored.

Required permission: datasheet:comment

Request body (JSON)

Optional

A comment id that does not belong to this datasheet is a 404.

DELETE /api/datasheet/:id/comments/:commentId

Delete your OWN comment on a datasheet. Ownership is enforced server-side.

Required permission: datasheet:comment