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`. EDIT VERSION: `datasheet.version` (an opaque string — never parse or change it) is what POST /:id/revise needs back as baseVersion.

Required permissions: datasheet:read, project:read

Optional permission (unlock additional behaviour): 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/:instanceId/revision/:revisionId instead (it returns elements in this same shape). 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" (id/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. It DOES carry `id`, which is what POST /:id/revise needs in changes[].element_id — so a compact read can drive a write without a second fetch.
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. Requires BOTH permissions: `project:read` on the datasheet's OWN project (the row question) and `datasheet:read` in any scope (the tier question). Access is their intersection.

Required permissions: datasheet:read, project:read

Optional permission (unlock additional behaviour): project:draft:view

GET /api/datasheet/:id/compare

Compare two datasheet revisions side-by-side. Requires BOTH permissions: `project:read` on the datasheet's OWN project (the row question) and `datasheet:read` in any scope (the tier question). Access is their intersection.

Required permissions: datasheet:read, project: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. Unknown top-level fields are REFUSED with a 400 naming them, rather than dropped. Every body field must have its documented type: name is a string, not a single-element array. summary and severity (MAJOR|MINOR|PATCH) describe the datasheet's FIRST revision; omit them for "Initial creation" / MAJOR. dispositions is an array of { element_id, disposition, disposition_note } for fields that start deferred — a deferred field carries no value, so leave it out of elementValues.

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, and is now REFUSED with a 400 naming every field it would have erased. The disposition is the only thing that carries the deferral; the empty value carries nothing, so a blank with nothing to say what it means is treated as an accident rather than obeyed. This is keyed on the ABSENCE of `disposition`, never on the emptiness of the value: to erase a value deliberately, send disposition: "specified" alongside the blank and it succeeds. To leave a field exactly as it is, OMIT it from changes[] — do not echo a whole datasheet back. Every success returns `changed` (how many fields this revision actually altered) and `cleared` (an entry per field whose recorded value was removed: element_id, label, previous_value), so an erasure is visible in the write response rather than only in a later compare. 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 — and pair it with disposition: "specified" to say so. 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. EDIT CONFLICTS: ALWAYS SEND baseVersion, the `version` from that same read (an opaque string — the head revision; never parse or change it). Missing, it is a 400 BASE_VERSION_REQUIRED for API keys and the assistant. Every success returns `version` for the next revise. If the datasheet moved on since your read, the server compares per field: when nobody else changed the fields you send, your changes are applied on top and the response carries merged: true and mergedChanges: [{ elementId, label, change, editedBy, editedAt }] naming the fields that came in from others; when someone changed one of your fields to a different value it is 409 `code: "EDIT_CONFLICT"`, metadata { reason: "fields", currentVersion, editedBy, editedAt, conflicts: [{ elementId, label, base, mine, theirs, editedBy, editedAt }] } (base/mine/theirs are { value, disposition, disposition_note }). To settle, revise again with baseVersion = currentVersion, leaving out each field whose other value you keep ("theirs") and sending each field you keep yours for ("mine"). A datasheet upgraded to another standard revision since your read is always 409 reason "upgraded" with metadata.standardRevision { id, code }: re-read it, never carry values across. The assistant never merges: any move since its read is 409 reason "stale". Nothing is written on a 409. On a 409, RE-READ the datasheet and reconsider: the other edit may already have done what was asked. 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). Resolve EVERY conflict in the plan and propose it: use the upgrade-plan suggestedValue when one is given, otherwise pick the option that best fits the datasheet as recorded and the standard text. For a definition_changed conflict the suggestedValue is the current value, which still meets the new rules: keep it. Never leave a conflict out and never ask first — the approval card lists each field with the value you chose, and the person reviewing it is the check. Say in the summary which choices were your own reading rather than the plan suggestion. Key each resolution by the integer elementId of the conflict from the upgrade plan (toElementId, or fromElementId for a removed field) or by its address ("5.4") — NOT by elementUuid: a 36-character id is easy to mis-copy, and two of 29 were on 2026-09-22. 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 (batch). CLOSED — answers 410 Gone to every caller. The legacy comment plane (standard_user_comments) is write-closed: commenting on the standards/wiki side is being replaced by a threaded discussion system, and the project-scoped half moved to project_comments. Existing comments stay READABLE through the GET endpoints and are neither deleted nor migrated. Project correspondence — including a comment on a datasheet field, via anchorType 'datasheet_field' — is authored on POST /api/projects/:projectId/comments.

Required permissions: datasheet:comment, project:read

Request body (JSON)

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

GET /api/datasheet/:id/comments

List comments on a datasheet instance. Omit standard_element_id for every comment, or filter to one field. Requires BOTH permissions: `project:read` on the datasheet's OWN project (the row question) and `datasheet:read` in any scope (the tier question). Access is their intersection.

Required permissions: datasheet:read, project: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. Requires BOTH permissions: `project:read` on the datasheet's OWN project (the row question) and `datasheet:read` in any scope (the tier question). Access is their intersection.

Required permissions: datasheet:read, project:read

Parameters
NameInTypeDescription
toRevisionIdqueryintegerTarget standard revision ID (defaults to latest)
conflictOffsetqueryintegerIndex of the first conflict to return (default 0). A large plan comes back one page at a time: read conflictPage.nextOffset from each page and pass it here until it is null. The upgrade needs a resolution for every conflict in conflictPage.total, not just the ones on the first page.
conflictLimitqueryintegerMost conflicts to return in one page. Omit it to get every conflict at once.

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

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

Required permissions: datasheet:comment, project:read

Request body (JSON)

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

GET /api/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. Also returns `collisions` — datasheets elsewhere in the SAME project carrying this name under a DIFFERENT standard. That is advisory, not a refusal: a datasheet name is an instrument tag, and one tag names one device, so a name that is `available` can still be a numbering collision worth raising with the user.

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 permissions: 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 read from that revision’s own snapshot, 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. Returns `elements` and `values_summary` in the SAME shape as GET /api/datasheet/:id, so one parser reads both; `include` and `format` behave identically there too. Requires BOTH permissions: `project:read` on the datasheet's OWN project (the row question) and `datasheet:read` in any scope (the tier question). Access is their intersection.

Required permissions: datasheet:read, project:read

Parameters
NameInTypeDescription
instanceIdpathintegerDatasheet instance id
revisionIdpathintegerDatasheet revision id belonging to that instance
formatquerystring"verbose" (default) or "compact" (id/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 compact shape omits drawing_ref, disposition and disposition_note, so it cannot answer which fields are deferred.
includequerystringComma-separated extras: "template" adds the standard's field definitions at this revision's standard revision, "comments" adds the comment thread. 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 accepted but is a no-op — elements always come back.)

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. Requires BOTH permissions: `project:read` on the datasheet's OWN project (the row question) and `datasheet:read` in any scope (the tier question). Access is their intersection.

Required permissions: datasheet:read, project: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. Requires BOTH permissions: `project:read` on the datasheet's OWN project (the row question) and `datasheet:read` in any scope (the tier question). Access is their intersection.

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

Required permissions: datasheet:comment, project:read

Request body (JSON)

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

PUT /api/datasheet/:id/comments/:commentId

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

Required permissions: datasheet:comment, project:read

Request body (JSON)

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

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

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

Required permissions: datasheet:comment, project:read