API reference

Connect AI agents through the hosted MCP server, or use the server-to-server REST API directly. Both surfaces provide governed access to a ManyRows project’s schema, records, collections, assets, compositions, and workflows.

Overview

The ManyRows Data API is a JSON HTTP API over one project’s records. Use it to sync records into your stack, drive a product or storefront from your ManyRows data, or build automations on top of your model.

  • Export a portable schema, then let an authorized agent add types, fields, collections, and configuration
  • Discover types and fields, then read and write records of any type
  • Structured query with filters, sorts, sparse fields, and cursor paging, plus project-wide search
  • A durable project change feed covering records, schema, catalogs, and configuration
  • Idempotent upsert by your own business key, plus ETag compare-and-swap
  • Read BOM structure, where-used, and the relationship graph; author BOM lines; substitute components
  • Explode a BOM into a parts summary, evaluate declared rollups, track life limits, trace recall impact
  • Propose changes to governed records through change requests (a human approves in the dashboard)
  • Manage specifications, drawings, certificates, and test reports as immutable controlled-document revisions
  • Collections, image and file uploads

For an AI client, the hosted MCP server is the recommended starting point: it exposes only the tools allowed by the connection and stays synchronized with the API contract. Use REST for application integrations, or generate a client from the canonical OpenAPI 3.0 contract. The shorter llms.txt entry point gives crawlers the canonical URLs and operating rules.

Schema changes are explicit

Record write access does not grant schema access. Enable Schema management on a dedicated API key when an agent should build the model. Imports are additive and never delete, rename, or rewrite an existing definition.

Quick start

Three calls to read and write your data. Set $BASE to your project’s data URL (see Base URL) and $KEY to an API key from the dashboard.

1 · Discover a type and its fields

discoverbash
curl "$BASE/types/part" -H "X-API-Key: $KEY"

2 · Create a record

createbash
curl -X POST "$BASE/entities" -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "part",
    "name": "M3 bolt",
    "referenceId": "m3-bolt",
    "attributes": { "unitCost": 0.12, "material": "steel" }
  }'

The 201 response is the new record (with an ETag header):

responsejson
{
  "id": "0a9f3c2e-...-e8",
  "typeKey": "part",
  "referenceId": "m3-bolt",
  "name": "M3 bolt",
  "attributes": { "unitCost": 0.12, "material": "steel" },
  "createdAt": "2026-06-22T10:04:11Z",
  "updatedAt": "2026-06-22T10:04:11Z"
}

3 · Query records

querybash
curl -X POST "$BASE/entities/query" -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{ "type": "part", "limit": 50 }'

That’s the core loop. From here, add filters and paging on query, update with PATCH / PUT, or make writes idempotent with upsert by reference.

AI agents and hosted MCP

Use MCP for ChatGPT, Claude, Codex, and other tool-using clients that support it. In the ManyRows dashboard, open the project, choose Schema → Connect AI, and copy the project-specific MCP URL. The same URL is also shown under API keys.

Recommended for AI-assisted schema design

Connect the project through MCP instead of downloading a static prompt or schema specification. The agent can inspect the live model, call the non-persisting schema validator, show you the plan, and then apply an additive import only when authorized. Ask it to validate before applying and to report every created, skipped, or rejected definition.

Canonical machine-readable contract

Fetch https://dash.manyrows.com/openapi.yaml before generating calls. It is public, requires no authentication, and is the only maintained OpenAPI copy. Every operation has a stable operationId for deterministic SDK method names and declares its authentication and authorization failures. Dashboard-only reference paths are marked x-manyrows-external: false; they are not external REST integration or MCP-tool operations. Recognized responses echo that identity in X-API-Operation-Id for support and telemetry correlation.

Explain a product release decision

After resolving the record, call GET /entities/{id}/release-brief (MCP get_entities_by_id_release_brief). It returns the latest revision, release blockers, required document evidence, inspection evidence, source links, and next actions from one consistent read. Coverage distinguishes complete, restricted, capped, unavailable, and unevaluated evidence. Certification remains a human decision; assembly configuration and shipment eligibility require separate assessments.

The brief preserves historical certificate evidence. It does not substitute today's inspection results for the evidence used at certification. Its ETag versions the visible brief; readinessVersion is the separate release-readiness token. Refresh before acting. The guide and brief have typed MCP output schemas; existing tool envelopes remain status, headers, and body.

Connect over MCP

Add the project-specific endpoint below as a remote HTTP MCP server. Prefer the client’s OAuth sign-in flow. If the client does not support OAuth, configure X-API-Key: <key> as a connection header; do not put credentials in the URL or tool arguments.

MCP endpointhttp
https://dash.manyrows.com/x/{workspaceId}/api/v1/projects/{projectId}/mcp

The hosted server is stateless and generates tools from externally available OpenAPI operations. It also exposes the canonical contract as manyrows://api/openapi. Read-only keys discover only read-safe tools; ordinary read-write keys receive record workflow tools; keys with Schema management also discover schema planning and import. MCP calls use the same validation, project isolation, limits, ETags, and error responses as REST.

For schema work, use a dedicated key with Schema management, or grant the OAuth scopes project:data:read project:data:write project:schema:manage. A practical first request is: Inspect this project's capabilities and current types. Propose a schema for my domain, validate it, summarize the plan, and wait for confirmation before applying it.

Connect through OAuth

In an OAuth-capable MCP client, enter the project MCP endpoint and follow the browser sign-in and consent flow. The server advertises authorization discovery through the WWW-Authenticate challenge and supports public clients using authorization code with PKCE S256. Request project:data:read, add project:data:write for edits, and add project:schema:manage with write access for schema management.

Delegated access is limited to the selected project, granted scopes, and the member's current permissions. OAuth access tokens are accepted only at that project's MCP endpoint; REST calls still require an API key. Manage and revoke grants from Connected agents in your profile. Workspace policy can disable agent delegation. Delegation does not grant human approval authority.

Tool arguments and results

Use tools/list to discover the tools available to this connection. Tool names follow the HTTP method and path: GET /capabilities becomes get_capabilities, and GET /entities/{id} becomes get_entities_by_id. Each input schema groups URL parameters under path, query parameters under query, and declared request headers under headers. Send the request payload as body.

MCP tool calljson
{
  "name": "patch_entities_by_id",
  "arguments": {
    "path": { "id": "<record UUID>" },
    "headers": { "If-Match": "<ETag from the record read>" },
    "body": { "name": "Updated part" }
  }
}

Results contain status, headers (each value is an array of strings), and body in structured content and a JSON text result. HTTP errors also set MCP isError; inspect the returned status and stable error code. For batch operations, HTTP 200 and a clear isError flag do not guarantee that changes were applied: also inspect applied, failed, and each item outcome. Preserve ETags from result headers for later conditional reads and writes.

For uploads, send body.fileBase64, body.fileName, and optional body.mediaType; the tool constructs the multipart request. Images are limited to 20 MiB decoded and other files to 10 MiB decoded; the complete base64 MCP request is limited to 30 MiB. For CSV imports, send the CSV as a JSON string in body. Set contentType when the tool schema offers multiple media types. Credentials belong on the MCP connection, not in tool arguments.

Coverage and authority

Every external Data API operation has a corresponding OpenAPI operation. Agent-callable operations generate MCP tools; operations marked x-manyrows-agent-restriction: human_only or retired remain documented but are omitted from the executable tool catalog. The canonical contract also retains dashboard-only reference paths marked x-manyrows-external: false; those paths do not produce external REST integrations or MCP tools. External coverage includes record and collection workflows, BOMs, assets, planning, quality evidence, change-request proposals, and capability-gated schema and configuration management. The tool catalog is filtered by access mode; individual calls also enforce project permissions and workflow state.

Dashboard administration has a separate API surface. Cost/pricing and supplier sourcing, rework and scrap execution, permanent deletion, webhook administration, release decisions, and human approval actions are not general agent capabilities. Some shared quality transition routes appear in the contract but require a signed-in human for approval, disposition, or verification; agents must hand those decisions to the assigned reviewer. Tool discovery does not override these checks.

Safe operating loop

  1. Discover, do not guess. Start with GET /agent-guide (MCP get_agent_guide) for project identity, effective access, task entry points, and human handoffs. Use GET /capabilities for detailed limits and workflow links. Call GET /types and use the returned type and field keys. Use GET /catalogs when classification matters.
  2. Read before changing. Fetch the record and retain its ETag. Use POST /entities/validate for a non-persisting check.
  3. Choose a retry-safe write. Prefer PUT /entities/by-reference/{type}/{referenceId} for synchronization, or PATCH a known id with If-Match. For a mutating POST, generate one Idempotency-Key per logical command and reuse it only when retrying that exact input.
  4. Plan every schema change. Use POST /schema/validate for additive definitions or POST /schema/migrations/validate for existing definitions. Require valid:true, inspect the report, and echo its ETag in If-Match on apply.
  5. Verify the outcome. Check the HTTP status and the stable error code, then read the affected record. Never infer success from prose in message.
  6. Resume durable work. Poll GET /changes, persist nextCursor only after processing the page, and deduplicate by change id.

Retry policy

  • Retry GET, query, validate, and identical upsert requests after transport failures. For a mutating POST, reuse the same Idempotency-Key only with the identical method, path, query, media type, precondition headers, and body. A completed response can be replayed for 24 hours and includes Idempotency-Replayed: true.
  • On 429, wait at least Retry-After seconds. On 500, use bounded exponential backoff with jitter and keep the X-Request-Id.
  • A 409 error.idempotencyInProgress includes Retry-After; retry the unchanged request with the same key. If it persists, contact support with the key and X-Request-Id so the outcome can be reconciled. The processing reservation does not expire automatically; using a new key could duplicate the command.
  • Do not replay a mutating POST without an idempotency key after an uncertain response. Read back by id/reference or inspect the affected resource first.
  • Do not retry other 4xx responses unchanged. Fix the input, credentials, governance flow, or stale ETag first.

Wire rules

Send JSON as UTF-8 with Content-Type: application/json except multipart asset uploads and documented CSV imports. JSON-compatible +json media types are accepted; an explicit non-JSON type returns 415 unsupported_media_type. URL-encode every path segment, especially referenceId. UUIDs and decimal quantities are strings where the schema says so; dates are YYYY-MM-DD, instants are RFC 3339. Omit optional fields you do not intend to change — JSON null can have clearing semantics on PATCH. Request objects are strict: unknown properties, trailing JSON documents, malformed flags, and undeclared query parameters are rejected rather than ignored.

Capability discovery

GET/capabilitiesRead

Bootstrap an autonomous client with the current API version, key access mode, hard limits, semantics, and relative links to the major workflows. Cache the response ETag and send If-None-Match to cheaply revalidate it.

Call this once at the start of an agent run instead of hard-coding operational limits. The returned access.readOnly and access.schemaManage reflect the key making the request; limits includes page, bulk, import, JSON-body, and change-feed retention limits; and every entry in links declares a relative href plus supported methods. Schema and configuration planning/apply links are only present when the key may use them.

Base URL

Every endpoint is scoped to a workspace and a project:

base urlhttp
https://<host>/x/{workspaceId}/api/v1/projects/{projectId}/data
  • host, workspaceId, and projectId are shown in the dashboard (the IDs are UUIDs).
  • v1 is the API version. A future breaking change ships as v2 alongside v1; v1 is never mutated in place.

All paths in this reference are relative to that base. For example POST /entities/query means a POST to .../data/entities/query. Examples below use $BASE for the base URL and $KEY for your API key.

Authentication

Create an API key in the dashboard and send it in either header:

headershttp
X-API-Key: mr_<prefix>_<secret>
Authorization: Bearer mr_<prefix>_<secret>

Key properties

  • Read-only, every mutation returns 403 error.readOnlyKey. Read-shaped POSTs remain available: record query and validation, POST /requirements/query material-plan query, batch lookup by reference, and bulk validation. Schema validators do not write either, but still require a separate Schema-management key. MCP independently exposes only read-safe tools to read-only keys.
  • Schema management is a separate opt-in for trusted automation. Without it, POST /schema/import returns 403 schema_manage_required. It cannot be combined with Read-only.
  • Project-scoped, a request for another project returns 401.
  • Expiry, after it expires the key returns 401.
  • IP allowlist, a request from a disallowed IP returns 403 error.ipNotAllowed (entries may be plain IPs or CIDR blocks).
  • Rotatable, a new secret is issued and the old one stops working immediately.
first requestbash
curl -s -X POST "$BASE/entities/query" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"type":"cymbal","limit":10}'

Limits

Rate limit

Per-workspace, per-minute — the budget is a workspace entitlement sized by plan, so all of a workspace’s API keys share one bucket (minting extra keys does not multiply the allowance). On a plan with a finite limit, every response carries the budget so you can back off proactively:

HeaderMeaning
RateLimit-LimitRequests allowed per window
RateLimit-RemainingRequests left in the current window
RateLimit-ResetSeconds until the budget refills

The legacy X-RateLimit-* names carry identical values. Exceeding the budget returns 429 with Retry-After. Unlimited (Enterprise) plans omit both header families. Monthly/lifetime call quotas are not enforced today.

Entitlement & body size

  • If billing is configured and the workspace has no active subscription (or its trial expired), requests return 402 error.trialExpired. When billing is unconfigured this gate is inert.
  • Request bodies are capped at 1 MB (asset uploads excepted); oversized returns 413.

Responses & correlation

  • Success bodies, when present, are JSON with Content-Type: application/json. A successful delete may return 204 with no body. Single-record reads and writes also send an ETag (the record’s version), see Concurrency.
  • Every response carries an X-Request-Id. Quote it when reporting a problem; it matches the server access log.
  • Every external response carries X-API-Version: 1 and a Link with rel="service-desc" to the canonical OpenAPI contract.

For high-volume writes, send Prefer: return=minimal. A successful mutation keeps its status, ETag, and other useful headers but omits the JSON body and returns Preference-Applied: return=minimal. Errors and preview/dry-run results always keep their bodies. Because the preference changes the response representation, it is part of an idempotent request’s identity.

Errors

Every error shares one envelope:

error envelopejson
{
  "error": "<code>",
  "message": "<human, advisory>",
  "issues": [ { "field": "...", "code": "...", "message": "..." } ]
}
  • error is a stable machine code, branch on this.
  • message is an advisory human string (always present).
  • issues appears on validation failures (error: "validation"), one entry per field problem; absent otherwise.

Two shapes of code

Gateway errors — raised by the auth, entitlement and rate-limit layer before your request reaches a handler — carry an error.-prefixed code. Endpoint errors are flat. Match the exact string including the prefix where shown: re-auth on error.unauthorized, not unauthorized.

Common codes

CodeHTTPMeaning
error.unauthorized401Missing / invalid / expired key, or wrong project
error.readOnlyKey403Write attempted with a read-only key
error.ipNotAllowed403Caller IP not in the key’s allowlist
error.trialExpired402No active subscription (billing configured)
error.tooManyRequests429Rate limit exceeded (see Retry-After)
error.internalError500Unexpected server-side failure — quote the X-Request-Id
not_found404No such record / type / route
invalid_json400Body was not parseable JSON
in_use409Referenced elsewhere; cannot proceed
cr_required409The type requires changes to go through a change request, see Propose changes
cr_locked409Record locked by an open change request (the body names it)
cr_not_author403Change request was not opened by this key
schema_manage_required403Enable Schema management on this API key before importing schema
schema_plan_required428Validate the schema and send the returned ETag in If-Match
schema_plan_stale412The project schema or proposed document changed after validation
schema_plan_invalid422Item errors blocked atomic apply; nothing was changed
governed_trash_requires_reason409Trashing a governed, BOM-used record needs a reason
precondition_failed412If-Match ETag no longer current
unknown_type400Type key / id not in this project
unsupported_media_type415Non-JSON media type on a JSON operation, or disallowed asset type
too_large413Body or upload over the cap
method_not_allowed405HTTP method not supported on the route
validation400Per-field problems in issues (codes: required, invalid, unknown_field, duplicate)

Text values are trimmed before they are validated or stored, so a required field is not satisfied by "" or " " — both come back as required. Send the value or omit the key; sending whitespace is the same as sending nothing. Trimming applies to text, long text and rich text, at the edges only.

Schema automation

Agents can bootstrap a new project and safely evolve existing definitions. Additive import uses a portable key-keyed document; governed migrations address existing types and fields by their stable keys.

GET/schema/exportRead

Export entity types, fields, base types, collections, catalogs, and supported configuration planes.

Schema exports return a strong ETag. Send it as If-None-Match on the next export; unchanged schemas return 304 with no body.

Portable primary structures and secondary planes preserve basisField, the junction field key for Fixed/Variable quantity basis, alongside role, yield and sequence mappings. The CPG starter includes this optional batch control; its existing ingredient examples remain Variable. Schema import is additive and skips existing structure owners. Reapplying a starter does not repair older configuration: set the quantity-basis mapping explicitly in the dashboard when updating an existing project.

POST/schema/migrations/validateSchema management

Plan updates to existing type and field metadata and invariants. Reports changes, affected records, stored values, attachments, views, errors, and blocked destructive work; the transaction is rolled back.

POST/schema/migrations/applySchema management

Atomically apply the validated migration and its security audit using the plan ETag. Key changes, field type changes, reference-target changes, and deletions are blocked.

POST/schema/validateSchema management

Run the real importer in a serializable transaction and roll it back. Returns the exact plan and an ETag without changing the project.

POST/schema/importSchema management

Atomically apply a validated plan. Requires its strong ETag in If-Match; any item error rolls back every definition.

export a portable authoring template (REST)bash
curl "$BASE/schema/export?template=true" -H "X-API-Key: $KEY" > schema.json

MCP clients do not need this template: their schema tools already expose the current request contract. The REST template remains available for offline generation, backups, and clients without MCP. Its $spec explains supported field types and configuration, plus record and whole-project conventions; import ignores $spec.

plan without changing anythingbash
curl -i -X POST "$BASE/schema/validate" \
  -H "X-API-Key: $SCHEMA_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @schema.json

The response body contains planHash, valid, and a report with four arrays: created, skipped, errors, and unapplied. Always inspect all four. unapplied names valid requested changes deliberately left alone because import is additive. Keep the strong ETag response header.

atomically apply the reviewed planbash
curl -X POST "$BASE/schema/import" \
  -H "X-API-Key: $SCHEMA_KEY" \
  -H "Content-Type: application/json" \
  -H "If-Match: $PLAN_ETAG" \
  -H "Idempotency-Key: schema-bootstrap-v1" \
  --data-binary @schema.json

Import returns 428 schema_plan_required without a plan ETag and 412 schema_plan_stale if either the project or proposed document changed. A late item error returns 422 schema_plan_invalid with the report and applies nothing. Successful applications and their plan hashes are recorded in the workspace Security audit.

Migrate existing definitions

A migration document can rename display labels, edit descriptions and defaults, change required/unique and supported type flags or select options, and mark definitions deprecated without deleting them. Omitted properties are preserved.

plan a governed migrationjson
{
  "version": 1,
  "entityTypes": [{ "key": "part", "name": "Component" }],
  "fields": [{
    "key": "material",
    "required": true,
    "deprecated": false,
    "config": { "options": [{ "value": "steel" }, { "value": "brass" }] }
  }]
}

Send this body to /schema/migrations/validate. A migration may contain at most 200 type and field items combined. A valid report lists ready items while applied remains empty. Review each item’s changes and impact, then send the identical body and ETag to /schema/migrations/apply. Any errors or blocked item makes the plan invalid and guarantees no changes are committed.

Schema discovery

Learn the field keys you need for filtering, sorting, and attribute payloads. Both endpoints are read-only, accept no query parameters, and return 400 for unknown ones.

GET/typesRead

List entity types with their fields, top-level total, deprecatedCount, builtInCount, and aggregate fieldCount summaries, plus fieldCount on every type.

GET/types/{key}Read

Get one entity type (by key or id) with its fields.

Both discovery responses return an ETag. Repeat the request with If-None-Match to receive 304 when its representation has not changed.

shapetext
GET /types        -> { "total": 4, "types": [ { key, name, fieldCount, nameUnique, builtIn, deprecated, fields:[...] } ] }
GET /types/{key}  -> { key, name, fieldCount, nameUnique, builtIn, deprecated, fields:[...] }

Each field descriptor is { key, type, required, deprecated, options?, targetType?, targetBaseType?, elementType? }. Deprecated definitions remain addressable for compatibility but should not be chosen for new work. options lists select / multi-select values; targetType is the referenced entity type’s key, while targetBaseType is used for a base-type target; elementType is a collection’s element kind.

GET /types/partjson
{
  "key": "part",
  "name": "Part",
  "nameUnique": false,
  "builtIn": false,
  "fields": [
    { "key": "unitCost", "type": "money",  "required": false },
    { "key": "material", "type": "select", "options": ["steel", "aluminium", "brass"] },
    { "key": "supplier", "type": "entity", "targetType": "supplier" }
  ]
}

Catalogs & categories

Catalogs classify records into a category tree. Discover them, then filter /entities/query by a category (the categories field).

GET/catalogsRead

The project’s catalogs. Each targets one entity type or base type and owns a tree of categories. Retain the response ETag and send If-None-Match to receive 304 while the catalog metadata remains unchanged.

GET/catalogs/{id}Read

Refresh one catalog directly, including its current category count. The response has its own ETag and supports If-None-Match.

GET/catalogs/by-key/{key}Read

Fetch the same conditional catalog representation through its portable stable key instead of a project-local UUID.

GET/catalogs/{id}/categoriesRead

A catalog’s categories, flat. Use /catalogs/by-key/{key}/categories when the portable catalog key is available. Nest by parentId (absent = a root). Each carries a rollup count — entities filed into it or any descendant. The response supports If-None-Match; its ETag changes for tree edits and assignment-count changes.

GET/catalogs/{id}/categories/{categoryId}Read

Refresh one category node and its subtree rollup count without downloading the full tree. The catalog-scoped response supports If-None-Match.

GET/catalogs/by-key/{key}/categories/by-code/{code}Read

Fetch one node through portable catalog and category selectors, with the same rollup count, effective dates, and conditional ETag.

GET/entities/{id}/categoriesRead

How one record is classified: for each catalog that applies to its type, the catalog’s category tree plus the record’s currentCategoryId (absent = unassigned). The response ETag versions the complete representation; send it as If-None-Match to receive 304 Not Modified when neither the tree nor its assignments changed.

PUT/entities/{id}/categories/{catalogId}Write

File one record with { "categoryId": "..." }. DELETE the same path to clear the assignment; clearing an absent assignment is idempotent. Both accept the GET ETag in If-Match and return the resulting ETag.

POST/entities/bulk-set-categoryWrite

File 1–200 unique records at once with { "catalogId": "...", "categoryId": "...", "entityIds": [...] }; the response reports set and inapplicable skipped counts.

POST/entities/bulk-sync-categoriesWrite

Reconcile up to 200 catalog assignments across up to 200 heterogeneous records. Target records by id or stable (type, referenceId); select catalogs by key/ID and categories by ID, unique code, or exact root/name path. Send category:null to clear.

{ "mode":"atomic", "preview":true, "items":[
  { "target":{ "type":"part", "referenceId":"SKU-42" }, "ifMatch":"\"9ab…\"",
    "assignments":[ { "catalog":"lifecycle", "category":"RELEASED" },
                    { "catalog":"region", "category":null } ] }
] }

Each record’s assignments are one isolated unit. Atomic mode is the default; best-effort commits valid records, while preview executes effective-date, applicability, working-copy, and selector validation in rollback-only savepoints. Results remain ordered with assigned, cleared, and unchanged actions. Use an Idempotency-Key when applying.

shapesjson
GET /catalogs                 -> { "catalogs": [ { id, key, name, kind, targetEntityTypeId?, targetBaseTypeId?, categoryCount } ] }
GET /catalogs/{id}/categories -> { "categories": [ { id, parentId?, name, code?, description, startDate?, endDate?, effective, position, count } ] }
GET /entities/{id}/categories -> { "assignments": [ { catalogId, catalogKey, catalogName, currentCategoryId?, categories:[…] } ] }

kind is entity_type or base_type (what the catalog classifies). Use a category id to filter records, see Query / list below. effective reflects the optional startDate/endDate window (today within the range, inclusive): records can only be newly filed into an effective category, existing assignments are kept.

Configuration snapshot

GET/configuration/exportRead

Read project configuration that does not belong in the portable schema document.

The versioned snapshot contains channels, channel groups, entity-type field projections, channel/group projection bindings, quick filters, curated-list definitions, and retained relationship definitions. It is project-scoped and assembled in one repeatable-read transaction, so its ETag always describes one coherent state.

configuration discoverybash
curl -s "$BASE/configuration/export" -H "X-API-Key: $KEY"
shapejson
{
  "version": 1,
  "channels": [...],
  "channelGroups": [...],
  "entityTypeViews": [...],
  "viewBindings": [...],
  "quickFilters": [...],
  "curatedLists": [...],
  "relationshipTypes": [...]
}

Curated-list membership and channel listings are record data, not configuration definitions, and are intentionally absent. Cache the response ETag and send If-None-Match; an unchanged snapshot returns 304.

Plan and import configuration

POST/configuration/validateSchema manage

Run the real configuration upserts in a serializable transaction, roll them back, and return a plan plus its ETag.

POST/configuration/importSchema manage

Atomically commit the validated document and its security-audit entry. Send the validation ETag in If-Match.

Imports support entity-type field projections, quick filters, and curated-list definitions. Every item requires a client-generated stable UUID and is upserted by that ID; an unchanged replay is reported in skipped. Existing scope and target IDs are immutable, and omitting a definition never deletes it. Send { "id": "...", "delete": true } for an explicit, idempotent deletion.

configuration documentjson
{
  "version": 1,
  "entityTypeViews": [
    { "id": "...", "entityTypeId": "...", "name": "Agent projection", "fieldIds": ["..."] }
  ],
  "quickFilters": [
    { "id": "...", "scopeKind": "type", "scopeId": "...", "name": "Needs review", "fieldKey": "status", "op": "eq", "value": "review", "position": 0 }
  ],
  "curatedLists": [
    { "id": "...", "name": "Launch set", "targetEntityTypeId": "..." }
  ]
}

The validation report separates created, updated, deleted, and skipped items. Its impacts array reports channel bindings for projection deletions and memberships that will cascade with a curated-list deletion. A bound projection is blocked until its channel/group bindings are removed; missing delete targets are safely skipped.

At most 200 total items may be sent. A normal read/write key cannot use these endpoints: the key must have schemaManage. Channels, channel-group bindings, curated-list membership, and retained legacy relationship definitions remain read-only and are rejected as undeclared document properties.

Query / list records

POST/entities/queryRead

List records of one entity type (or one base type) with structured filters, sorts, and paging. Read-only keys may call it despite the POST method.

Request body

FieldTypeMeaning
type / baseTypestringScope to one entity-type key/id, or a base-type key/id (exactly one required)
qstringSubstring match on name / reference_id
filtersarrayConditions to apply (see below)
match"all" | "any"Join filters with AND (default) or OR
sortsarraySort order (see below)
fieldsstring[]Sparse selection, return only these keys in each record’s attributes (top-level fields always included)
scopestringWhich lifecycle plane to list: active (default), archived, draft, trashed. On any plane but active, filters and sorts are dropped — those are flat, recency-ordered lists
trashedbooleanLegacy shorthand for scope: "trashed". Don’t send both: trashed: true forces the Trash plane unless scope is "archived"
updatedAfterRFC3339Records modified strictly after this time (incremental sync)
effectiveOndateAs-of lens, YYYY-MM-DD or RFC3339: keep only records whose effectivity window covers that day (day-granular, both bounds inclusive)
notInCollectionuuidExclude records already in this collection (picker use)
unreferencedOnlybooleanRestrict to records no live record references — the "unused" view
categoriesarrayClassification filters, records filed into a catalog category (see below)
limitintegerPage size, default 50, max 200 (over-max clamps to 200)
offsetintegerPage offset (offset-based paging)
cursorstringOpaque keyset cursor for stable large-dataset iteration
countTotalbooleanDefault true. Set false when paging purely to collect ids — see below

Skipping the counts. Every page carries a total, and an active-scope page also carries trashedTotal / archivedTotal / draftTotal. Each is a separate scan of the same predicate, so walking a large result set pays four of them per page for numbers a bulk enumeration never reads. Send "countTotal": false to skip all four; they come back as -1, which is visibly “not counted” — 0 would be indistinguishable from an empty result. The rows are unchanged, and omitting the field means true, so existing callers keep the totals they expect.

Filters

filter entriesjson
{ "field": "status", "op": "eq",  "value": "active" }
{ "field": "score",  "op": "gte", "value": 90 }
{ "field": "tag",    "op": "in",  "values": ["a", "b"] }
{ "field": "note",   "op": "blank" }
  • Use value (string, number, or boolean; coerced per field type) for scalar ops.
  • Use values (array of scalars) for in / not-in.
  • Omit both for blank / present.

Operators: eq, neq, contains (text only, case-insensitive), gte, lte, blank, present, in, not-in. neq / not-in treat an unset field as "not equal". Builtins: name (eq, neq, contains), reference_id (eq, neq, contains, in, not-in), id (eq, neq, in, not-in). Not every operator applies to every field type, discovery plus a 400 tell you which.

Sorts

Each entry is { "field": "created_at", "dir": "desc" }. dir is asc (default) or desc. Builtins: name, reference_id, created_at, updated_at; or any field key.

Category filters

Restrict to records filed into a catalog category. Get the ids from GET /catalogs and GET /catalogs/{id}/categories (see Catalogs & categories). Each entry ANDs:

categories entryjson
{ "catalogId": "...", "categoryId": "...", "includeSubtree": true }

includeSubtree (default false) also matches records filed into any descendant category — e.g. filtering by Components with includeSubtree: true returns everything under it. Omit categoryId (or send "") to match records filed into any category of the catalog — a catalog-only filter, e.g. { "catalogId": "..." }.

Response

responsejson
{ "entities": [ ... ], "total": 123, "trashedTotal": 4,
  "limit": 50, "offset": 0, "nextCursor": "eyJjIjoibmFtZS..." }

Cursor pagination

Recommended for iterating large sets: when the response includes nextCursor, pass it back as cursor in the next request body. The same token is exposed as X-Next-Cursor, and a counted query exposes X-Total-Count, for clients that want pagination metadata before decoding JSON. Keyset paging doesn’t skip or duplicate rows under concurrent writes the way deep offset does. The cursor is valid for a single builtin sort (name / reference_id / created_at / updated_at, including the default name); use a single-entry sorts with that column. An absent cursor/header means you’ve reached the end. cursor overrides offset when both are supplied.

filter + sort + limitbash
curl -X POST "$BASE/entities/query" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{
    "type": "contact",
    "filters": [{ "field": "status", "op": "eq", "value": "active" }],
    "sorts": [{ "field": "created_at", "dir": "desc" }],
    "limit": 50
  }'

Incremental sync

Poll with updatedAfter and an ascending updated_at sort, checkpointing the largest updatedAt you’ve seen. Treat re-delivered rows as idempotent upserts.

incremental syncbash
curl -X POST "$BASE/entities/query" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"type":"cymbal","sorts":[{"field":"updated_at","dir":"asc"}],
       "updatedAfter":"2026-06-22T00:00:00Z"}'

Durable project change feed

GET/changesRead

Walk committed record, schema, catalog, and configuration signals in a stable oldest-first order, then resume from an opaque checkpoint.

Use the feed for a durable pull integration: local search indexes, warehouse mirrors, agent work queues, and recovery after a webhook receiver was unavailable. Start without a checkpoint (or with an RFC 3339 since), process the page, then persist nextCursor. Pass that token as cursor on the next request. Never construct or edit a cursor, and keep the same project and filters for its entire walk.

poll changesbash
curl -s "$BASE/changes?limit=100&type=part&operation=update" \
  -H "X-API-Key: $KEY"
responsejson
{
  "changes": [{
    "id": "019b...",
    "resource": "record",
    "event": "entity.updated",
    "operation": "update",
    "resourceId": "019a...",
    "resourceType": "record",
    "resourceKey": "M3-BOLT",
    "refreshHref": "/entities/019a...",
    "entityId": "019a...",
    "entityType": { "id": "0198...", "key": "part", "name": "Part" },
    "referenceId": "M3-BOLT",
    "version": "1788684150123456000",
    "source": "api",
    "changedAt": "2026-09-06T12:42:30.123456Z",
    "tombstone": false
  }],
  "nextCursor": "eyJjIjoiY2hhbmdlcyIsLi4ufQ",
  "hasMore": false,
  "retentionDays": 30
}
  • At least once. Deduplicate on the stable event id, and save the page cursor only after every item in that page is safely applied.
  • Signals, not changed values. Each item identifies a resource plane and specific resourceType. refreshHref gives the relative endpoint to fetch. Record signals point to the record; portable model changes point to /schema/export; channels, projections, quick filters, curated lists, and relationship definitions point to /configuration/export.
  • Deletion is explicit. A removal has tombstone: true. Deleted records omit refreshHref; remove them from the mirror. Definition removals still point to /schema/export or /configuration/export, whose current snapshots are authoritative.
  • Filter at the source. Repeat resource (record, schema, catalog, or configuration) and operation. A repeated type key or UUID restricts the feed to record changes and cannot be combined with a non-record resource.
  • Cheap empty polls. nextCursor is echoed when no changes are available. Cache that representation’s ETag, send If-None-Match, and accept 304. The checkpoint is also returned in X-Next-Cursor.
  • Retention is explicit. retentionDays and GET /capabilities report the window (zero means forever). An older checkpoint returns 410 change_cursor_expired; discard it, refresh both definition snapshots and fully resync records, then start a new feed checkpoint.

Older record events retained across the rollout may omit referenceId and version. Process every signal idempotently: one logical update can emit several fine-grained signals, while one read of the supplied snapshot endpoint covers all of them.

Get · create · replace · patch · delete

GET/entities/{id}Read

Fetch one record by id.

GET/entities/by-ids?ids=a,b,cRead

Batch fetch by id, any type (≤ 200).

GET/entities/by-reference/{type}/{referenceId}Read

Fetch by your business key.

POST/entities/by-referencesRead

Batch fetch up to 200 { type, referenceId } pairs in request order, with a result for every input.

POST/entities/operational-snapshotsRead

Read coherent operational context for up to 100 records in one repeatable-read transaction. Select from record, categories, collections, structure, rollups, requirements, sizeSpec, sizeCurve, taSchedule, quality, releaseReadiness, and configurationReadiness. Each successful section carries its own semantic ETag; target and section failures stay in request order without discarding usable data.

curl -sS -X POST "$BASE/entities/operational-snapshots" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"targets":[{"type":"order","referenceId":"PO-1042"}],"sections":["record","categories","collections","requirements","sizeCurve","taSchedule"],"collectionFields":["components","certifications"],"asOf":"2026-09-07","structureDepth":10}'

targets accept either id or type plus referenceId; duplicate aliases of the same record are rejected in place as duplicate_target. On later polls, add an ifNoneMatch object to each target, keyed by selected section; matching sections return notModified:true and their ETag without retransmitting data. Optional fields sparsifies only the record section. The categories section returns the applicable trees, current assignments, and an ETag ready for bulk-sync-categories. For collections, omit collectionFields to discover every readable attached collection, or select up to 50 field keys/IDs. Each field returns its complete ordered members, stable member IDs, and its own ETag ready for bulk-sync-collections; missing, non-collection, duplicate, and denied selectors are explicit per-field outcomes with succeeded/failed counts, and make the target partial. asOf and asOfUnit apply the supported effectivity lenses. This POST is a read and works with read-only keys.

POST/integrity/scanRead

Scan a cursor-paged slice of the project for cross-record drift: legacy missing required values, references to non-live records, malformed or cyclic BOMs, one-to-many violations, inverted effectivity, classification mismatch, governed records without revisions, and inconsistent staging metadata.

curl -sS -X POST "$BASE/integrity/scan" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"scope":"all","checks":["required_fields","references","structure","effectivity","classification","governance","staging"],"limit":100,"structureDepth":20}'

Follow nextCursor until complete is true—even a page with no findings may have another page. Each stable finding code includes severity, the affected record and ETag, optional related record/field, and advisory repair endpoint. The scanner honors field access, runs in a read-only repeatable-read transaction, changes nothing, and works with read-only API keys.

POST/entities/duplicate-candidatesRead

Find explainable duplicate pairs within one type. Names and reference ids are normalized; up to ten selected field keys are compared by exact rendered value. Results include a 1–100 score, the contributing reasons, and both records’ versions and ETags. The scan is deterministic and bounded to 2,000 records, with populationTruncated telling an agent when it should narrow or partition its cleanup.

curl -sS -X POST "$BASE/entities/duplicate-candidates"   -H "X-API-Key: $KEY" -H "Content-Type: application/json"   -d '{"type":"part","fields":["manufacturerPartNumber"],"threshold":70,"limit":50}'

Selected fields must be attached to the type and readable by the caller. This is a read-safe POST and works with read-only keys; archived records are excluded unless includeArchived is true.

POST/entities/mergeWrite

Preview, resolve, then atomically merge a duplicate into a survivor. Preview lists every automatic field/collection/category transfer, inbound reference and relationship rewrite, plus blockers. Differing field values require fieldSources choices; differing classifications require categorySources choices.

# Preview first
curl -sS -X POST "$BASE/entities/merge"   -H "X-API-Key: $KEY" -H "Content-Type: application/json"   -d '{"preview":true,"survivor":{"id":"..."},"duplicate":{"id":"..."},"fieldSources":{"manufacturer":"survivor"}}'

# Apply the same plan with both preview ETags and an idempotency key
curl -sS -X POST "$BASE/entities/merge"   -H "X-API-Key: $KEY" -H "Idempotency-Key: merge-part-1042" -H "Content-Type: application/json"   -d '{"preview":false,"survivor":{"id":"..."},"duplicate":{"id":"..."},"survivorIfMatch":""..."","duplicateIfMatch":""..."","planIfMatch":""..."","fieldSources":{"manufacturer":"survivor"}}'

Apply requires survivorIfMatch, duplicateIfMatch, and the preview’s planEtag as planIfMatch. The plan validator covers collections, classifications, and inbound links too, preventing an apply from silently absorbing changes that did not affect either record ETag. A successful apply keeps the survivor’s identity, unions safe collections and source-only classifications, repoints safe inbound field and graph references, then archives the duplicate with supersededBy pointing to the survivor. The tombstone, revisions, audit trail, and owned operational history remain recoverable. The merge refuses field-denied, governed, frozen, draft, or staged records; governed/frozen referrers; junction or attributed-edge records; BOM-owning duplicates; cardinality collisions; graph duplicates/self-links; and BOM cycles. Move a duplicate’s own BOM lines with the structure endpoints before retrying.

POST/entitiesWrite

Create. 201; body { type, name, referenceId?, attributes }.

PUT/entities/{id}Write

Full replace. 200; omitted attributes are cleared.

PATCH/entities/{id}Write

Partial update. Only the keys you send change; a JSON null clears a field, an omitted field is untouched.

DELETE/entities/{id}Write

Soft-delete to Trash. 204; idempotent. Send the record ETag as If-Match to reject a stale delete with 412. Optional body { "reason": "..." } — required when the record’s type is change-controlled and the record is used in a BOM (else 409 governed_trash_requires_reason, listing the affected BOM parents).

attributes is keyed by field key; values are bare JSON. referenceId is your portable business key; it auto-generates from the name if omitted on create.

Single-record GETs by id or reference return an ETag. Send If-None-Match to receive 304 when unchanged. Sparse fields requests always return their selected representation rather than incorrectly reusing the full-record validator.

How values are encoded

Field typeJSON value
Text · Long text · Select · URLstring, e.g. "steel"
Integer · Decimal · Money · Percentnumber, e.g. 0.12
Booleantrue / false
DateISO 8601 string, e.g. "2026-06-22"
Multi-selectarray of strings
Entity (reference)the target record’s id string
Collectionmanaged via the collection-member endpoints, not in attributes
Image · Filethe descriptor returned by an asset upload

For the exact, authoritative per-type rules, export with ?template=1, the response’s $spec documents the encoding with a worked example.

patch one fieldbash
curl -X PATCH "$BASE/entities/$ID" -H "X-API-Key: $KEY" \
  -d '{"attributes":{"size_label":"20"}}'

Idempotent upsert

PUT/entities/by-reference/{type}/{referenceId}Write

Create-or-update keyed on your own id, retries are safe (no duplicate). 201 created | 200 updated.

Body is { name, attributes }; referenceId comes from the URL. If a record with that reference exists it is replaced; otherwise it is created. This is the recommended way to make writes idempotent.

upsert by your keybash
curl -X PUT "$BASE/entities/by-reference/part/m3-bolt" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{ "name": "M3 bolt", "attributes": { "unitCost": 0.14 } }'
# -> 201 created  |  200 updated  (run it again, same result, no duplicate)

If the entity type requires a change request on create (requiresCrOnCreate), an upsert that creates a record returns it as a draft (draftedAt set) instead of publishing it live — see Duplicate & drafts. It must be activated before it appears in queries; subsequent upserts to the same referenceId update that draft in place.

Concurrency (ETag / If-Match)

Reads and writes return an ETag (the record’s version). To avoid clobbering a concurrent update, echo it back on a write with If-Match:

compare-and-swapbash
ETAG=$(curl -sD- "$BASE/entities/$ID" -H "X-API-Key: $KEY" -o /dev/null \
  | tr -d '\r' | awk -F': ' '/^ETag/{print $2}')

curl -X PATCH "$BASE/entities/$ID" -H "X-API-Key: $KEY" \
  -H "If-Match: $ETAG" -d '{"name":"New"}'
# -> 412 precondition_failed if the record changed since you read it

If-Match is optional; without it a write is unconditional. With it, the write is an atomic compare-and-swap on the record’s version, so concurrent writers can’t lose an update.

Validate (dry run)

POST/entities/validateRead

204 if valid, else the same validation body a create returns.

POST/entities/bulk-validateRead

Validate up to 200 create-shaped records without persisting them. Results remain in input order and every item carries an issues array, empty when valid.

Bulk & trash

Bulk record commands are capped at 200 items/call. Selection-based operations use this compact result envelope:

bulk resultjson
{ "succeeded": <count>, "failed": [ { "id": "...", "code": "...", "message": "..." } ] }
MethodPath
POST/entities/bulk-delete
POST/entities/bulk-restore
POST/entities/bulk-set-field
POST/entities/bulk-update
POST/entities/bulk-upsert
POST/entities/bulk-activate
POST/entities/bulk-sync-collections
POST/entities/bulk-sync-categories
POST/entities/bulk-duplicate
POST/entities/bulk-archive
POST/entities/bulk-unarchive
POST/entities/bulk-apply-size-spec-template
POST/entities/bulk-apply-ta-template
GET/entities/counts
GET/entities/{id}/reference-count
GET/entities/{id}/reverse/{fieldKey}

bulk-update is the heterogeneous agent write: each ordered item has target (either id, or type plus referenceId), a partial patch, and optional per-record ifMatch ETag. It runs the same permissions, governance, field validation, revision, and audit pipeline as an individual PATCH.

atomic heterogeneous updatejson
{
  "mode": "atomic",
  "preview": false,
  "items": [
    { "target": { "id": "..." }, "ifMatch": "\"1720000000000000000\"", "patch": { "name": "New name" } },
    { "target": { "type": "part", "referenceId": "SKU-42" }, "patch": { "attributes": { "season": "FW27" } } }
  ]
}

mode defaults to atomic: one failed item rolls successful items back and marks them rolled_back. Use best_effort to commit valid items while returning failures beside them. Set preview:true to execute all checks without committing; successful results are would_update. A processed command returns 200, so branch on top-level applied and each result’s status, httpStatus, error, and issues. Supply an Idempotency-Key when applying it.

bulk-upsert is the full-record synchronization command. Each item carries a stable type plus referenceId, the replacement name/attributes, and an optional ifMatch. It returns ordered created, updated, or staged outcomes; preview uses would_create, would_update, and would_stage. New records honor draft:true and governed-create rules. Governed live updates fail with cr_required and a changeRequestPath, preserving human approval.

preview a mixed-type synchronizationjson
{
  "mode": "atomic",
  "preview": true,
  "items": [
    { "type": "part", "referenceId": "ERP-100", "name": "M3 bolt", "attributes": { "finish": "zinc" } },
    { "type": "supplier", "referenceId": "SUP-9", "name": "Fasteners Ltd", "ifMatch": "\"1720000000000000000\"" }
  ]
}

bulk-set-field takes { "type": ..., "ids": [...], "fields": [ {"field": "season", "value": "FW26"}, {"field": "shipDate", "clear": true} ] } — one or more fields, each set (or cleared) to one shared value across the selection; the whole request is rejected if any entry is invalid.

bulk-delete moves records to the per-type Trash (recover with bulk-restore). bulk-archive hides records from grids and pickers while keeping them live in existing references (reverse with bulk-unarchive). Both take { "ids": [...] }. bulk-delete also accepts an optional reason — required when any selected record is change-controlled and used in a BOM; without one the whole call is refused with 409 governed_trash_requires_reason, naming the affected BOM parents.

Permanent deletion is not exposed to API keys. /entities/bulk-purge, /entities/empty-trash, /entities/delete-all, and /entities/wipe return 404 on this server-to-server API. Purging Trash is an admin-dashboard action.

The two bulk-apply-…-template ops take { "entityIds": [...], "templateId": "..." } and apply one measurement (size-spec) or Time & Action template across the selection, skipping names already present (idempotent); the response tallies { entities, added, skipped }. Size specs are product-definition data: the whole request is refused (409) if any selected live record is change-controlled or locked by an open change request. T&A is operational data and applies to governed records too; only change-request working copies are refused.

Import / export

GET/entities/export?type={key}Read

Add ?template=1 for a fillable template, or ?allPlanes=1 to include draft & archived records (with effectivity) for a full round-trip. Returns up to 10k records per call — resume with ?offset=.

The export can be narrowed to a subset rather than the whole type. Everything below is ignored when ?ids= names an explicit selection, which stands on its own:

Query paramMeaning
idsComma-separated record ids — export exactly these
fieldsComma-separated field keys to limit the exported attributes (default all importable fields)
qSubstring match on name / reference id
filterRepeatable, colon-delimited key:op[:value] — the same operators as a query filters entry
matchall (default) or any
categoryRepeatable, colon-delimited: catalogId, catalogId:categoryId, or catalogId:categoryId:1 for that category and its subtree
unreferencedOnly1 / true for records nothing live references
effectiveOnAs-of lens, YYYY-MM-DD or RFC3339

A malformed category, filter, effectiveOn or offset is a 400, never a silently unscoped export — a dropped scope would return every record and look exactly like a complete one.

POST/entities/import/validateRead

Preview the exact single-type import path in a rolled-back transaction. Returns the same created, updated, failed, and ignored-key report as apply without changing records; ≤ 10000 records.

POST/entities/importWrite

{ type, records:[...], draft? }, upsert by referenceId; ≤ 2000 records. Set draft: true to stage rows for review (imports into change-controlled types). Successful rows and their audit trail commit as one request-level transaction.

POST/entities/import-multi/validateRead

Preview the dependency-ordered multi-type import in a rolled-back transaction, including every per-type failure and ignored key.

POST/entities/import-multiWrite

{ types:[{ type, records:[...] }, ...] }, referenced types are imported first and the request commits atomically on infrastructure success.

The export response is { type, version, $spec, fields, total, truncated, nextOffset?, records:[...] }. $spec and fields are always present and teach how to author records, $spec carries the per-type value-encoding rules and a worked example; fields lists each attribute with its type, select options, and reference targets. truncated is true when the 10k export cap cut it short — the response then carries nextOffset; pass it back as ?offset= to fetch the next window, repeating until truncated is false.

The import response is { succeeded, created, updated, failed, ignoredKeys }. ignoredKeys lists attribute keys that aren’t fields on the type; they are skipped (not an error), so a typo’d key surfaces here instead of vanishing silently.

Prefer MCP for AI-generated records

An MCP-connected agent can discover fields and validate or write records directly without moving a static specification through a prompt. Keep ?template=1 for offline or non-MCP batch workflows that need a portable $spec and field list.

Preview a CAD snapshot

In the app, open Import a bill of materials from the entity grid or Components tab and choose Import CAD snapshot JSON instead. Select a normalized snapshot for the current entity type, review source-to-target matches and per-parent quantities, then apply. Imports opened from a record must match that assembly. Native CAD files need an exporter first. After an apply error, refresh the preview before retrying; an already accepted current snapshot is acknowledged without repeating writes.

POST/cad/snapshots/previewWrite

Validate an exporter-produced engineering BOM and preview proposed part and line operations. UTF-8 JSON only, optional retainedAfter and retainedLimit query parameters for retained-line pagination, at most 2 MiB. CAD preview, import and binding commands reject duplicate object keys, malformed Unicode and incorrectly cased field names with 400 invalid_json. Source identities are preserved without text replacement or normalization; control characters are rejected. No CAD application is contacted.

counted CAD snapshotjson
{
  "schemaVersion": 1,
  "type": "part",
  "source": {
    "system": "custom-exporter",
    "documentId": "assembly-document-104",
    "version": "immutable-snapshot-7",
    "configuration": "default",
    "exporterVersion": "1.0"
  },
  "complete": true,
  "rootId": "assembly/default",
  "components": [
    { "id": "assembly/default", "referenceId": "ASM-104", "name": "Assembly" },
    { "id": "bolt/default", "referenceId": "BOLT-M6", "name": "Bolt" }
  ],
  "usages": [
    { "id": "bolt-left", "parentId": "assembly/default", "componentId": "bolt/default", "quantity": "1", "unit": "each" },
    { "id": "bolt-right", "parentId": "assembly/default", "componentId": "bolt/default", "quantity": "1", "unit": "each" }
  ]
}

Each component ID identifies one configured source component. Each usage belongs to its immediate parent definition; do not repeat a shared subassembly’s internal usages for each placement. Repeated parent-component usages are summed exactly (the example produces quantity "2"). Version 1 supports positive integer counts with unit each; lengths, masses, and fractional quantities are rejected. Provide at most 2,000 components, 10,000 usages, and 1,999 distinct parent-component pairs.

Supply an explicit root and complete snapshot, including stable document, version, configuration, and exporter identifiers. Use usages: [] for a single component. Incomplete graphs, cycles, unreachable components, duplicate IDs, and distinct configured components sharing a part number are rejected. Source version and completeness are assertions from the exporter, not independently verified against a CAD vendor. Unknown properties are rejected. Optional component attributes can initialize new parts as documented below.

The response contains preview: true, snapshotSha256, the snapshot sorted by component/usage ID, a normalized import body, summary counts, and itemized parts and lines. Part actions are create or match; line actions are create or update with the proposed quantity. Existing records include their entity IDs. Update actions include quantities that already match.

Preview executes the existing BOM import validations and rolls back records, proposed bindings, audit entries, and webhook events. It requires write permission; field restrictions, frozen records, change-request locks, and governed-type restrictions still apply. Existing source bindings take precedence over part numbers (matching: "source_binding_then_part_number"). Unbound components match by part number and propose new bindings. resolutions shows each component ID, source part number, target part number, and any existing target/binding IDs. bindingsCreated counts proposed new bindings; sourceBindingsPersisted: false means preview saved none.

Onshape exporter

Download the standalone exporter and setup guide from the app’s CAD snapshot import dialog. It requires Python 3.10+ and no source checkout or additional Python packages. The script (maintained as integrations/onshape/export.py) reads a named-version Onshape BOM through API v17 using local request-signature credentials. Its fetch, inspect, and convert commands produce a snapshot for this review flow. Conversion requires explicit part-number, name and counted-quantity column IDs plus a quantity basis (per-parent or root-total). Shared assembly definitions and exact counts are preserved; incomplete or ambiguous inputs fail. Capture and snapshot files are published only after writing completes, without replacing existing files; the output folder must support hard links. The optional --property-columns JSON argument maps explicit BOM column IDs to Manyrows field keys for new parts; it does not convert units. Standard content, non-geometric items, attachments and automatic synchronization are not supported. The exporter has synthetic contract tests but has not been validated with an authenticated Onshape account. It is not a hosted OAuth connection or an App Store integration.

Inventor exporter

The CAD snapshot import dialog also offers a standalone Inventor exporter and guide. It requires Windows, a running Inventor instance and the .NET 10 SDK. It reads a saved Structured/All Levels BOM in the primary model state, using native count quantities. Merged/promoted rows, enabled occurrence instance properties, overrides, parameter-based units, custom/substitute states, iParts and iAssemblies are rejected. Instance properties can distinguish native BOM rows that snapshot v1 would otherwise combine. The tool checks the selected document path and rechecks source stamps after extraction; it does not change BOM settings or save CAD files. Document identity combines normalized path and internal ID, so file moves change source identity. Synthetic collector and server-contract tests pass, but the actual Inventor COM runtime is unvalidated. Review the generated JSON before applying. No Vault connection, attachments or automatic synchronization are included.

Apply a CAD snapshot

POST/cad/snapshots/importWrite

Send { snapshot, snapshotSha256, expectedImportId, expectedBindingsSha256 } using the reviewed preview response, with expectedImportId copied from sourceState.importId ("none" before the first accepted import). For a new application, copy expectedBindingsSha256 from preview.bindingsSha256 too. Missing it returns 428; changed mappings or resolved part-number targets return 409 cad_bindings_changed and require a fresh preview. Parts, BOM lines, source bindings, and the new receipt commit atomically. JSON only, no query parameters, at most 2 MiB.

A new successful apply returns applied: true, preview: false, sourceBindingsPersisted: true, and durable entity/binding IDs in resolutions. Re-import resolves each configured component to its bound entity ID, including after a source or target part-number change. CAD does not rename target records or overwrite existing part attributes. Additional line fields are preserved; missing lines are never deleted. The legacy import body remains available for inspection, but applying it through /entities/import-bom does not use or persist source bindings.

A mismatched source fingerprint returns 409 cad_snapshot_changed. Missing expectedImportId returns 428 cad_preview_required; if another import has advanced this source stream since preview, apply returns 409 cad_source_changed. Preview returns the current receipt without advancing it. A stream is scoped by project, target type, source system/document/configuration, and root component ID. The receipt protects against competing imports in that stream. The binding fingerprint separately detects mapping creation, removal, recreation or reassignment for components in the snapshot, under the same lock used by binding changes. It also detects part-number matches changing between create and match, to another record, or to a renamed bound target. Current identical retries remain no-ops even if mappings changed and do not require the binding fingerprint. The fingerprint also detects creation, deletion, replacement or edits of incoming BOM lines, including quantities and extra fields, regardless of which stream edited them. Existing line updates use the checked version; an edit or deletion detected after line lookup returns 409 import_conflict and rolls back the entire preview/import. Preview again before retrying. Preview lines include optional previousQuantity: the exact stored quantity before writes, omitted for new lines or unset values. Zero is preserved as a string; stored quantities may be fractional. The dialog shows current and proposed quantities side by side. It distinguishes new lines, changed quantities and unchanged quantities using exact decimal comparison. Hide unchanged quantities filters the review table only; the complete snapshot is still submitted, and refreshing the preview resets the filter. Equal quantities do not imply a no-op import: existing lines remain update operations and source metadata may be recorded. retainedLineCount reports existing nondeleted lines outside change requests, directly under matched snapshot components, that are absent from incoming update targets. It includes matched leaves, excludes descendants of omitted components, and is measured before writes. Those lines remain unchanged; the dialog warns when the count is positive. Optional retainedLines lists up to 200 entries ordered by line entity ID, with entityId, parentReferenceId, childReferenceId and optional exact-string quantity. When retainedHasMore is true, pass retainedNextCursor as retainedAfter to the same preview route with the same snapshot. Optional retainedLimit is 1–200 (default 200). The dialog provides Previous/Next to review all retained lines. Paging reads live information; refresh after BOM edits. Unset quantities are omitted and unavailable child references are empty. The details are informational, outside the fingerprint, and omitted on replay. This informational count is outside bindingsSha256 and omitted on replay. In the CAD import dialog, Refresh preview reuses the selected file and resets assembly confirmation. The selected file is cleared when the dialog closes or its target changes. Lines outside the incoming graph, part attribute values and schema changes are not covered. CAD and ordinary BOM previews/imports share the project composition lock with BOM-editor and bulk composition operations, covering graph reads, cycle checks and writes. Direct entity writes do not all acquire that lock, so line-version checks remain necessary. The fingerprint is opaque; refresh older previews before applying.

A previously accepted version with different content fails with 409 cad_source_version_conflict. A previously accepted version that another import has superseded fails with 409 cad_source_replay, even if the caller supplies the current receipt. An entity-scoped retry for another record, or for a receipt whose original target cannot be verified, fails with 409 cad_replay_root_mismatch. Older receipts without a recorded target are verified against the original import audit event using its receipt ID, source version and snapshot hash; current mappings do not establish past import targets. Version identities must represent immutable snapshot content, including exporter settings; corrected extraction needs a new identity.

Previewing or retrying the current identical version returns replayed: true, applied: false, zero operation counts, and empty operation/resolution lists. It writes no records, bindings, audit entries, or new receipts and does not restore later manual edits or removed bindings. This acknowledges the accepted receipt, not the current validity of its targets. Existing type and permission gates still apply. Use the binding list to recover target IDs after a lost response.

Source versions are opaque and not verified against a vendor: an older vendor version that this server has never accepted cannot be distinguished from a genuinely new version. Exporters must still verify upstream chronology and keep stream identities stable. Imports made before receipt tracking have no backfilled history; their first receipt establishes the baseline. For a new version, a missing, archived, staged, or otherwise unusable bound target returns 409 cad_binding_target_unavailable instead of falling back to a reused part number.

GET/cad/snapshots/stateRead

Require type, system, documentId, configuration, and rootId. Returns { importId, version?, snapshotSha256?, importedAt? }. Before the first receipt for that root, importId is "none"; the component may still have been imported inside another assembly. In CAD tab, select Show assembly import receipt on a mapping to view the accepted version, time, receipt ID and snapshot hash. Refresh receipt reads the current accepted state again. Receipts belong to source streams and survive later mapping changes. This reports accepted-import state, not vendor freshness or current target contents.

Manage CAD source bindings

POST/cad/bindingsWrite

Atomically bind 1–2,000 source components to existing records. JSON body ≤ 2 MiB, no query parameters.

explicit source mappingjson
{
  "type": "part",
  "source": { "system": "custom-exporter", "documentId": "assembly-document-104", "configuration": "default" },
  "bindings": [ { "componentId": "bolt/default", "entityId": "<existing-part-uuid>" } ]
}

Binding identity includes the project, target type, source system/document/configuration, and configured component ID; source version is separate. Choose a system namespace that distinguishes source accounts or tenants. These identifiers are caller-supplied, not vendor-verified. Repeating an identical mapping is a no-op. A source component cannot switch targets, and two source components in one scope cannot bind to the same target: conflicts return 409 cad_binding_conflict and roll back the complete request. Binding changes are audited. They may repair source metadata on approval-governed types, but frozen targets and targets covered by a pending change request are refused.

GET/cad/bindingsRead

Require type, system, documentId, and configuration. Optional limit (1–200, default 100) and offset (0–1,000,000). Returns { bindings, hasMore, nextOffset }; follow nextOffset only while hasMore is true.

DELETE/cad/bindings/{id}Write

Explicitly remove a binding by immutable ID; returns 204. Does not delete the target record. A future import may match by part number again, so review the remapping before re-importing. Deleted targets retain their binding until explicitly removed. The CAD tab offers this action with confirmation; the BOM and accepted receipt remain unchanged.

GET/entities/{id}/cad/change-requestRead

Find a draft or submitted change request covering this record before starting a CAD import. Returns { changeRequest: null | { id, title, status, cadProposal } }. Ordinary change requests are included because they also block a CAD import. The CAD tab opens the pending request and hides the import action until it is approved, rejected, or discarded. A missing record returns 404.

GET/entities/{id}/cad-bindingsRead

Discover CAD mappings for one record across source systems, documents and configurations, without knowing the source identifiers first. Available in the record’s CAD tab. Returns { bindings, hasMore, nextOffset }, ordered by binding ID. Limit defaults to 50 (1–200); offset defaults to zero (0–1,000,000). Missing or deleted records return 404. Paging is live; refresh from the first page after mapping changes. Mappings do not establish a live connection or report the latest CAD revision. The entity CAD tab sits after Compositions for composition-enabled types outside working copies. Use the adjacent CAD and Compositions tabs for source review and the BOM. Successful imports refresh source details and BOM data. Read-only, frozen, archived and draft records cannot import. Approval-required types offer Add CAD source → Create change request → Review change request: submit through the usual approval workflow. File attachments stay in Documents. Add CAD source opens guided setup for Onshape, Inventor, Fusion or SOLIDWORKS, with prerequisites, exporter downloads and the target type key. Existing snapshot files can be selected directly. Try a sample assembly previews a three-component example using the selected record in both direct and approval workflows; it requires BOM configuration and preview permission, saves nothing, and cannot be applied from the dialog. Other CAD programs have no ready-to-run exporter here yet. Approval-required records use POST /entities/{id}/cad/preview (snapshot body) and POST /entities/{id}/cad/propose (snapshot plus snapshotSha256, expectedImportId and expectedBindingsSha256). Propose returns staged:true, applied:false, changeRequestId and workingCopyId. Live BOM, mappings and the accepted receipt change only at approval, atomically. The root must match this record; existing parent assemblies must be in its working-copy tree. Existing pending requests conflict. Preview and proposal creation also check every existing matched leaf for freezes, pending requests and source-mapping conflicts before creating a draft. These checks publish no mappings or audit events and are repeated at approval. After a lost proposal response, inspect Changes & releases for the created draft. Changed bindings or source heads reject approval with 409 cad_proposal_changed. Discard/reject publishes no source receipt. Reviewers can edit drafts; source receipts describe provenance, not final BOM equality.

Send CAD snapshots directly for review

Open Send to Manyrows in the entity’s CAD import dialog to download the standalone sender and its guide. Requires Python 3.10+ and a read-write Manyrows project API key. Generated commands include the exact entity API URL and type; updates also include the complete source identity. Run the connection check, then export and send with Onshape, Inventor or SOLIDWORKS. Fusion exports inside its application: save the snapshot there and run the sender’s send command locally. You no longer need to select that file in the browser.

For repeat use, choose Download desktop package in Send to Manyrows. Extract the ZIP and open start.cmd on Windows or start.command on macOS. The package includes the helpers, selected exporter, destination settings and guides. Python 3.10+ with Tk is required. Named exporter fields replace manual JSON editing; Check setup diagnoses missing files, the .NET SDK and running CAD processes. Check connection explains rejected keys and permissions. Delivery results link back to the destination’s CAD tab. Exporter failures show redacted error details in the desktop window. Retry saved snapshot verifies unchanged file contents and retries delivery without exporting again; failed exports preserve the previous snapshot selection. Reopening the package restores its own settings and last retry file. For Onshape, Capture from Onshape fetches a named-version assembly using a pasted link and session-only vendor API keys. Select the captured BOM columns and quantity basis before exporting. New captures clear old column mappings; failed captures preserve the previous capture and retry file. Fusion exports inside Fusion. Desktop windows scroll on smaller screens, and Open another assembly opens an independent profile. Optional remembered keys use macOS Keychain or Windows Credential Manager through the Python keyring package, with no plaintext fallback. Sending still requires review and normal approval.

Snapshots appear under Received snapshots after refreshing the CAD tab. Select Review snapshot to run a fresh preview, then apply or create the usual change request. Sending does not change the BOM or bypass approval. Connection checks verify current target access and readiness; local exporters check their own prerequisites when run. Neither check reserves records or certifies compatibility with an installed CAD runtime.

Enter your key at the sender’s hidden prompt or provide MANYROWS_API_KEY through your local environment. Credentials are not stored by the sender or shared through setup. HTTPS is required except loopback development; redirects are refused and TLS verification stays enabled. The sender retries temporary network and HTTP 429/500/502/503/504 failures at most three times using identical bytes. After an uncertain result, run send with the same saved file. Do not re-export merely to retry delivery.

GET/entities/{id}/cad/connectionRead

Returns ready, reason, writable, requiresApproval, entityId, type, referenceId, maxSnapshotBytes (2097152) and maxPending (20). Reasons include root_unavailable, no_composition, component_type_mismatch, write_permission_required and pending_change_request; empty means ready. Does not contact CAD software.

POST/entities/{id}/cad/submissionsWrite

Send the snapshot body, at most 2 MiB strict JSON. Normalized JSON must leave 1024 bytes for eventual apply metadata. Type and root must match the selected record. The direct/governed importer runs in a rolled-back preview, then saves the submission and a metadata-only audit event. Returns {submission, created}: 201 when newly queued or restored after proposal rejection/abandonment, 200 when an existing receipt is returned. At most 20 pending payloads (409 cad_queue_full). Identical retries return the same pending, imported, proposed or dismissed receipt without repeating imports. A rejected or abandoned proposal can be sent again with the same saved snapshot; current lifecycle, source and mapped-field checks run again before its pending payload is restored under the same receipt ID. Dismissed receipts remain closed.

GET/entities/{id}/cad/submissionsRead

Newest-first metadata with limit 1–50 (default 20), offset 0–1,000,000 (default zero). Returns submissions, hasMore and nextOffset. Each submission has id, entityId, snapshotSha256, source, rootId, status and createdAt. Status is pending, imported, proposed, dismissed, rejected or abandoned; active and rejected proposals may include changeRequestId. No mapped values or snapshot payloads are returned. Paging is live; refresh from the first page after new deliveries.

GET/entities/{id}/cad/submissions/{submissionId}Read

Returns metadata plus snapshot only while pending. Reading a payload rechecks field restrictions and schema compatibility. Run the fetched snapshot through a fresh entity-scoped preview before guarded import/propose; submission-time state is never an apply token.

DELETE/entities/{id}/cad/submissions/{submissionId}Write

Dismiss a pending payload and retain its receipt, with an attributed audit event. Returns 204; known terminal receipts remain unchanged. Sending the same content again returns dismissed. Deliberate reconsideration can use the file importer. Does not undo a BOM import or cancel a change request.

A matching import or proposal consumes the stored payload in the same transaction. Proposal approval marks its receipt imported. Rejection marks it rejected and retains the change-request link; deleting that request marks it abandoned and clears the link. Neither decision automatically restores the payload. To review the same snapshot again, send its saved file; the server validates it afresh and restores the pending payload on the existing receipt. Terminal payloads are removed and small receipts retained for retry safety. Type, entity or project deletion removes submissions. CAD source chronology remains the exporter’s responsibility.

Import history and shared setup

Open Import history on a CAD source to see accepted versions and compare part numbers and per-parent quantities with the previous import. These are differences between CAD source snapshots, not a record of manual BOM edits or draft changes. Omitted source lines remain in the BOM. Earlier receipts without stored source graphs remain listed, with comparison unavailable. Previews and identical retries create no history; governed imports appear only after approval.

GET/cad/snapshots/historyRead

Required query parameters: type, system, documentId, configuration, rootId. Optional limit 1–50 (default 20), offset 0–1,000,000 (default 0). Returns imports, hasMore, nextOffset; each entry has importId, version, importedAt, hasGraph and optional previousImportId. Newest first; refresh from the first page after another import. A component imported only as a child may have no root history.

GET/cad/snapshots/history/{id}Read

Returns available, parts and lines. Part changes have componentId and optional before/after source part numbers. Line changes have parent/component source part numbers and optional before/after exact quantity strings. An absent side means added or omitted from the source snapshot. The first import compares with an empty graph; unavailable comparisons return empty arrays. Graphs store identities and counts, never mapped attribute values.

GET/cad/bindings/{id}/settingsRead

Returns revision, settings and redacted. Before first save: revision 0, settings null. Restricted destination mappings are omitted and redacted is true. Shared setup belongs to this project and binding; removing the mapping removes its setup.

PUT/cad/bindings/{id}/settingsWrite

Replace with {expectedRevision, settings:{program, command, properties}}, at most 64 KiB strict JSON. Use the read revision (zero for first save); stale writes return 409 cad_settings_changed. Reload and review before retrying. Program must match the source: onshape, inventor, solidworks or fusion. Command supports Onshape part/name/quantity column IDs and basis (per-parent or root-total), Inventor namespace, or SOLIDWORKS namespace/property. Fusion uses an empty command. Property rows contain field/source, plus group/name only for Fusion custom attributes. Onshape source is a column ID; Fusion supports material, description, massKg and custom. At most 100 distinct accessible scalar fields; native exporters accept no property rows. Paths, shell preferences, labels, unit confirmations and credentials are rejected. Restricted existing mappings prevent replacement. Frozen targets and pending requests block writes. Saving setup does not change the BOM or confirm numeric units.

CAD setup and selected assembly

GET /entities/{id}/cad/readiness returns {ready, reason, requiresApproval}. Reasons are an empty string when ready, root_unavailable, no_composition or component_type_mismatch. It checks lifecycle and composition compatibility before the CAD tab enables import. It is advisory and does not grant write permission or reserve the record. Pending change requests are checked separately.

For ungoverned records, use POST /entities/{id}/cad/import/preview with the snapshot, then POST /entities/{id}/cad/import with the snapshot and preview guards. The root must resolve to this record through source bindings or part number; otherwise 409 cad_root_mismatch. Accepted retries must belong to this record too (409 cad_replay_root_mismatch). Approval-required records retain the entity CAD preview/propose routes. Global snapshot routes remain available for integrations creating a new assembly.

Mapped CAD properties

Components may include an optional attributes object mapping Manyrows field keys to scalar values, for example {"material":"Steel","mass":"0.125","purchased":true}. Values initialize new parts only; existing parts retain their attributes. The preview shows values and the policy for each part. Text, long text, integer, decimal and boolean fields are supported. Unknown, unsupported or denied fields fail. Nulls, arrays and objects are rejected. Maximum 100 keys per component and 16384 JSON bytes per value within the 2 MiB request limit. Decimal values must be JSON strings in the destination unit; no unit conversion occurs. Integer properties are limited to -9007199254740991 through 9007199254740991 for exact browser handling; larger or fractional values require decimal strings. Properties participate in the snapshot fingerprint and immutable source version, and new-part values follow the same atomic import or staged approval as the BOM. Ordinary BOM import fields remain unsupported.

Fusion exporter

The CAD dialog also downloads a Python script and setup guide for Fusion. Run the script inside Fusion on a saved, fully processed, unconfigured design with local components. It exports immediate-parent counts once per component definition, includes hidden components and rejects external/configured/derived occurrences, library fasteners, suppressed timeline features, rolled-back timelines and unsaved edits. Optional mappings cover assigned component material, calculated leaf mass in kg, description and custom API attributes. Component material does not resolve body-level material overrides. This is a component-tree adapter; it does not apply arbitrary manufacturing BOM rules. Synthetic tests cover conversion; real Fusion runtime validation is outstanding. Inventor currently exports structure and identity without property extraction.

CAD property setup

Use Update from CAD on a source mapping to import another version of that same document, configuration and root component. The browser rejects a different source before preview; existing server-side target, approval and stale-preview checks still apply. Exporter setup can be remembered locally in this browser, scoped to the signed-in account, project, entity type and source/root. This includes paths, namespace, column choices, quantity basis and property mappings, but no credentials or captured model data. Use Save setup for project to share reusable exporter options and property mappings for an existing source. Shared settings take precedence over local reusable options. Paths, shell preferences and captured column labels stay local. A teammate can load the shared setup without inheriting another machine’s file paths. You can disable remembering or forget saved settings. Property fields are rechecked and numeric unit confirmations must be repeated when restoring mappings.

In the import dialog, select your CAD program and open Export settings. The entity type is filled in and the required root assembly part number is shown. For Inventor and SOLIDWORKS, enter the saved assembly path and a stable source namespace to generate a PowerShell command. For Onshape, first capture the named version using the setup guide, then choose the local capture file and select part-number, name and quantity columns by their displayed names. Choose the quantity basis explicitly to generate a conversion command. Capture files are limited to 16 MiB and are read locally, without uploading them. Selecting a new capture clears previous column and quantity-basis choices. Fusion uses its in-app script prompt. Commands do not rename CAD parts; set the matching root part number in CAD before exporting.

The optional mapping controls list supported, accessible destination fields with GET /cad/property-fields?type=KEY, returning {fields:[{key,name,type}]}. Denied fields are omitted. Select Onshape columns from a local capture file or choose named Fusion properties. Numeric mappings require a unit check; Fusion mass is in kg. Download the mapping file and select it in Fusion or pass it to Onshape convert with --property-map-file. Generated Onshape mappings include each destination type, so whole-number source values can safely map to decimal fields. Values with unit suffixes or grouping separators are rejected; no unit conversion is performed. Files are limited to 64 KiB and 100 fields. The original inline Onshape --property-columns option remains available; it cannot be combined with the file option. Import validates permissions and values again. Validation errors identify the affected source component or parent/child pair when an input index is available; mapped-property binding errors include the component part number and field. The dialog provides correction guidance for property types, quantities, duplicate identities and stale previews.

SOLIDWORKS exporter

Download the standalone exporter and guide from the CAD dialog. Requires Windows, SOLIDWORKS 2019+ and .NET 10. Reads saved, resolved counted component trees with explicit part-number properties. Rejects unsupported component states, exclusions, unloaded children, quantity units, pending rebuilds and inactive configurations. Drawing table overrides and cut-list expansion are unsupported. Stable identity uses normalized path plus configuration; file moves change identity. Two source reads and saved-file fingerprints guard extraction. Output never overwrites an existing file. Synthetic API tests cover the adapter; real SOLIDWORKS runtime validation remains pending.

Importing a bill of materials

POST/entities/import-bomWrite

Import an indented BOM — the parts and the structure between them, in one call. Normalize CAD exports to the contract below before importing. This endpoint does not read native CAD files.

Structure is carried by a level column rather than stated: each row belongs to the nearest row above it whose level is one lower. Parts are created or matched by referenceId (the part number), so re-importing after a design change updates the tree instead of laying a second one beside it, and a part appearing under several parents stays one record with several lines.

json bodyjson
{
  "type": "part",
  "dryRun": false,
  "rows": [
    { "level": 0, "referenceId": "ASM-100", "name": "Gearbox" },
    { "level": 1, "referenceId": "SHF-12",  "name": "Shaft",  "quantity": "1" },
    { "level": 1, "referenceId": "BRG-04",  "name": "Bearing", "quantity": "2" }
  ]
}

For CSV, POST text/csv with ?type=part and optionally &dryRun=true (only true or false). JSON carries both options in its body and rejects query parameters. Both formats accept at most 2,000 rows; CSV has an 8 MiB body limit, and upstream request limits can be stricter.

CSV requires an explicit Part Number column and the assembly as the first data row. Recognized English header aliases are supported; duplicate logical columns are rejected. Extra named columns are ignored, and every nonblank data row must have the same number of cells as the header (column_count otherwise). Descriptions never become part numbers. Nonempty JSON fields maps fail with unsupported_fields; custom property mapping is not supported.

Hierarchy uses one convention throughout: a nonnegative integer Level column (zero is valid), numeric dotted item paths such as 1.2.1 with preceding parents, or consistent indentation of two spaces or one tab per level. An explicit Level column takes precedence. Include one root, then its descendants; later rows at or above the root level, skipped levels, malformed paths, and duplicate parent-component pairs are rejected. Multiple existing lines for the same pair fail with ambiguous_line.

Send quantities as plain decimal strings such as "2.5". Unit suffixes (including ea), decimal commas, grouping separators, exponents, and negative values are rejected. Non-root quantities must also pass composition field validation, including its positive-quantity rule. Blank or omitted quantities leave existing quantities unchanged; new lines use the configured defaults and validation.

The response is { partsCreated, partsMatched, linesCreated, linesUpdated, rootReferenceId, dryRun? }. Existing parts retain their attributes. Existing lines retain other line fields and effectivity; lines absent from the input are not deleted. A component used under different parents remains one part record with separate usage lines.

With dryRun: true, the same writes and validations run inside a transaction that is rolled back before success. Records, audit entries, and webhook events are not persisted. Preview requires write permission and does not reserve a future commit: apply validates current state again.

Invalid imports are rejected atomically. Validation errors return 400; oversized CSV returns 413; missing composition configuration returns 422. A governed type returns 409 cr_required and requires the change-request workflow. If a line changes between the import’s read and update, 409 import_conflict rolls back the import: preview again and retry. Imports of the same type serialize with each other; this does not lock all manual or schema edits.

Duplicate & drafts

Duplicate a record

POST/entities/{id}/duplicateWrite

Deep-copy one record. The body selects what to carry over (an omitted/empty list copies none of that category).

duplicatejson
{
  "name": "Copy of Widget",
  "copyAttributeFieldIds": ["<fieldId>", "..."],
  "copyCollectionFieldIds": ["<fieldId>", "..."],
  "copyComponents": true,
  "copyChildren": [ { "typeId": "<child entity-type id>", "fieldId": "<forward entity-field id on that type>" } ]
}

copyComponents copies the BOM / structure subtree. Field ids come from Types & fields. Returns the new record.

copyChildren deep-copies one-to-many children onto the clone: for each {typeId, fieldId} group, every entity of typeId whose fieldId points at the source is cloned full-fat and re-pointed at the new root (one level only, ≤200 children per group). Discover the copyable groups with GET /entities/{id}/child-ref-groups → { "groups": [ { typeId, typeKey, typeName, fieldId, fieldLabel, count } ], "groupCount": N, "total": N }. Each group includes the stable typeKey for portable automation; unsupported query parameters return a structured 400. The discovery response has an ETag; resend it in If-None-Match for a 304 when unchanged. When sent, the response gains a children envelope alongside the new entity: { "succeeded": N, "failed": [...] }.

Draft activation

Records can be created as drafts: not live yet, hidden from live reads, and editable without a change request. Drafting defers the change request — a change-controlled type takes one at activation rather than on every edit — not the rules: a draft is validated like any other record, required fields included, so an incomplete write is rejected whether or not draft is set. Promoting a draft activates its whole reference closure at once.

GET/entities/{id}/activation-closureRead

Preview: { entities:[...] } that would activate together.

POST/entities/{id}/activateWrite

No body. Promote the closure to Active. Send the root record ETag as If-Match to reject activation with 412 if the draft changed after review.

activate returns { "activated": [ids...] } (200) on a direct publish. If any member of the closure belongs to a type that requires a change request, it stages one instead and returns { "changeRequest": {...} } (201, pending) — approving that request is a dashboard action, so a key-staged activation waits for a human approver.

POST/entities/bulk-activateWrite

Activate up to 200 ordered draft roots by id or (type, referenceId). Every item requires its current ifMatch ETag. Preview first, then apply atomically (default) or with best-effort isolation and an idempotency key.

curl -sS -X POST "$BASE/entities/bulk-activate" \
  -H "X-API-Key: $KEY" -H "Idempotency-Key: activate-import-1042" -H "Content-Type: application/json" \
  -d '{"mode":"atomic","preview":true,"items":[{"target":{"type":"part","referenceId":"ERP-100"},"ifMatch":"\"1720000000000000000\""}]}'

Outcomes include each closure’s ids and are activated, staged, covered, would_*, rolled_back, or failed. A subordinate or duplicate target is covered by the earlier closure; partial overlap fails with activation_closure_overlap so two governed requests cannot claim one draft. Governed closures return their change-request id/path and still require human approval.

Propose changes (change requests)

Writing directly to a record whose type requires a change request returns 409 cr_required. Propose the change instead — open a change request, edit the working copy it hands you, submit, poll:

the propose loophttp
POST   /change-requests            {"title":"CAD sync: wing geometry","rootEntityId":"<id>"}
        -> 201 {"changeRequest":{"id":"<crId>",...},"rootWorkingCopyId":"<wcId>"}
PATCH  /entities/<wcId>            edit the WORKING COPY, not the live record
POST   /change-requests/<crId>/submit
GET    /change-requests/<crId>     poll changeRequest.status

In the dashboard, My Work, the Assigned review inbox, Awaiting me, and approval badges include active standing approver delegations. They show unsigned slots in the first stage whose quorum is still unmet; future stages and spare reviewers from completed stages are excluded. Once all stages meet quorum, eligible approvers and their active delegates can still find a pending final-apply action. Requests already marked ready for package application leave these personal approval queues. Standing approver delegation is a human review workflow, separate from OAuth agent authorization.

Opening clones the record (and its BOM subtree) into staging and returns rootWorkingCopyId — every edit goes to that id with the normal record endpoints, and the live record is untouched until the change request is approved. Submitting moves the draft into review; when you name no approvers, the type’s default roster applies, exactly as from the dashboard. Change requests a key opens are attributed to apikey:<keyName>.

GET/change-requestsRead

List the project’s change requests, paged (limit / offset; total is the full match count). Readable by any key — change requests are project data.

POST/change-requests/by-idsRead

Read up to 200 exact statuses in one call: { "ids": ["...", "..."] }. Results preserve order and duplicates, with item-level invalid_id and not_found outcomes. Found rows include lifecycle and decision fields, the named roster, an approval rollup, and package readiness. Approved requests still resolve from terminal history after their working copies are removed. This read-safe POST works with read-only keys and needs no idempotency key.

POST/change-requestsWrite

Open a change request on a governed record: { "title": "...", "rootEntityId": "..." } → 201 with the change request plus rootWorkingCopyId. Created as a draft; it enters review only on submit. Idempotent for a root already staged under your draft; 409 if another open change request covers the record.

POST/change-requests/bulk-fieldWrite

The batch counterpart — one field set across many records (a nightly ECO run, a season rollover). Same propose-only semantics: created for review, not applied.

POST/change-requests/bulk-updateWrite

Stage up to 200 different governed PATCHes at once. It accepts the same id-or-reference targets, per-item ifMatch, atomic/best-effort modes, and preview behavior as record bulk-update. Each success returns its change-request id, working-copy id, detail path, and submit path; live records remain untouched.

governed batch proposaljson
{
  "title": "FW27 agent changes",
  "mode": "atomic",
  "preview": true,
  "package": { "title": "FW27 release set", "justification": "Review together" },
  "items": [
    { "target": { "id": "..." }, "ifMatch": "\"1720000000000000000\"", "patch": { "name": "Revised" } },
    { "target": { "type": "part", "referenceId": "SKU-42" }, "patch": { "attributes": { "material": "Recycled nylon" } } }
  ]
}

The optional package groups newly created entity change requests for the human review and release workflow. Submit the returned drafts individually through each submitPath, or submit the whole set with bulk-submit below. API keys cannot approve or release the package. In package mode, an already-existing draft is returned as cr_already_exists instead of being silently moved out of its current review set.

POST/change-requests/bulk-submitWrite

Submit up to 200 drafts for human review in one command. Provide either ordered items or a packageId; package members expand in creation order. Shared classification and approvers apply to the batch, with per-item overrides for an explicit list. Supports atomic (default), best_effort, and preview. Every draft must have been opened by this API key.

validate and submit a review setjson
{
  "mode": "atomic",
  "preview": true,
  "classification": "major",
  "requiredApprovers": ["[email protected]"],
  "items": [
    { "id": "<crId>" },
    { "id": "<crId>", "classification": "minor", "requiredApprovers": [] }
  ]
}

An empty per-item requiredApprovers override selects that record type’s configured defaults. Each ordered result is submitted, would_submit, rolled_back, or failed. Poll all returned ids together with POST /change-requests/by-ids. To submit a package instead, replace items with packageId; package submissions use the shared classification and approvers.

POST/change-requests/bulk-abandonWrite

Clean up to 200 drafts after an automation run. Select an ordered ids list or one packageId, and use atomic (default), best_effort, or preview. Only drafts this API key opened are eligible; item failures distinguish not_found, not_draft, and cr_not_author. Working copies and never-live staged rows are removed, activation drafts return to Drafts, and live records are untouched.

preview draft cleanupjson
{
  "mode": "atomic",
  "preview": true,
  "ids": ["<crId>", "<crId>"]
}
GET/change-requests/{id}Read

Read one change request; its status tells you whether your proposal is still in review.

POST/change-requests/{id}/submitWrite

Move the draft into review. 403 cr_not_author unless this key opened it; 409 unless it is still a draft.

Optional body { "requiredApprovers": ["[email protected]"], "classification": "minor" | "major" }. Both may be omitted: no approvers means the type’s default roster, and an absent classification means major, the conservative default. An unrecognised classification is rejected rather than quietly downgrading review.

DELETE/change-requests/{id}Write

Abandon one change request this key opened (403 cr_not_author otherwise). 204. For safe automated cleanup, prefer the draft-only bulk endpoint above.

Approving is a human act

The approve and reject endpoints simply do not exist on this API — a person decides in the dashboard. Poll changeRequest.status: open while in review, and the change request disappears once applied (its history lives on as the record’s revisions).

Collection members

Members of a collection field on a record (paged):

GET/entities/{id}/collections/{fieldKey}/membersRead
PUT/entities/{id}/collections/{fieldKey}/membersWrite

Atomically make the collection match one complete ordered list of up to 200 values. Existing equal values retain their member IDs; the result reports added, removed, moved, and unchanged members.

Use preview: true to validate and inspect the reconciliation without persisting it. GET and PUT return an ETag; a preview returns the unchanged baseline validator, which you should echo in If-Match on the applying request. A concurrent edit then returns 412 instead of being overwritten. Send an explicit empty members array to clear the collection.

preview an ordered relationship syncbash
curl -X PUT "$BASE/entities/$RECORD/collections/components/members" \
  -H "X-API-Key: $KEY" -H 'Content-Type: application/json' \
  -H 'If-Match: "4f2a…"' \
  -d '{"preview":true,"members":[{"value":"'$PART_A'"},{"value":"'$PART_B'"}]}'
POST/entities/bulk-sync-collectionsWrite

Reconcile up to 200 complete ordered collections across different record types and fields. Each item uses an id or stable (type, referenceId) target, a field key or id, its own required collection ifMatch, and its complete members list.

preview a heterogeneous collection batchjson
{
  "mode": "atomic",
  "preview": true,
  "items": [
    { "target": { "type": "part", "referenceId": "ASM-100" }, "field": "components", "ifMatch": ""4f2a…"", "members": [{ "value": "<partId>" }] },
    { "target": { "id": "<supplierId>" }, "field": "certifications", "ifMatch": ""83bc…"", "members": [{ "value": "ISO 9001" }] }
  ]
}

The response stays in request order and embeds each individual reconciliation with its added, removed, moved, and unchanged summary. atomic is the default and marks earlier successes rolled_back if any item fails; best_effort commits valid items. Preview successes are would_sync. Duplicate aliases of the same record and field are rejected, and each item follows the individual endpoint’s access, cardinality, freeze, governance, stable-member-ID, and audit rules. Supply an Idempotency-Key when applying the batch.

POST/entities/{id}/collections/{fieldKey}/membersWrite
POST/entities/{id}/collections/{fieldKey}/members/bulk-deleteWrite

Remove several members at once: { "ids": [...] } (or { "all": true }).

DELETE/entities/{id}/collections/{fieldKey}/members/{memberId}Write
PATCH/entities/{id}/collections/{fieldKey}/members/{memberId}Write

Reorder.

Structure tree (BOM)

GET/entities/{id}/structureRead

The descended composition tree rooted at {id}.

Returns { "configured": false } when the record’s type has no structure configured. Otherwise: root, nodes[] (each with level, position, cycleCut, the per-line line attributes, and a child summary with hasChildren), plus quantityKey and lineColumns describing the junction’s scalar columns. nodeCount reports line occurrences while uniqueChildCount reports distinct child records; directNodeCount reports immediate lines, and terminalNodeCount/expandableNodeCount divide the returned frontier. byLevel/byTypeKey group occurrences, while uniqueByTypeKey groups distinct records. Traversal diagnostics include cycleCutCount, maxDepth, and depthLimitReached. The HTTP ETag covers the complete returned tree for If-None-Match polling. The body version identifies the direct line set; quote it for an atomic bulk edit’s If-Match.

Each node’s child.state is active, archived, or draft, so automation can reject non-live components without another read.

child.version is the child record’s unquoted optimistic-concurrency token for safe follow-on writes.

child.etag is the quoted form ready for an If-Match header.

Each node’s line etag is likewise quoted and ready for a composition-line update; its sibling version remains unquoted.

Each node’s alternateCount reports how many approved alternates are available on that line.

alternates[] includes each alternate’s member and entity IDs, identity, image URLs, and readable attributes in configured order.

Every alternate includes lifecycle state, allowing agents to reject archived or draft substitutes immediately.

Every alternate also includes its unquoted record version for concurrency-safe follow-on writes.

The sibling alternate etag is already quoted for direct If-Match use.

root.state carries the same lifecycle vocabulary for the assembly itself.

root.version is the assembly record’s unquoted optimistic-concurrency token; it is separate from the top-level direct-line-set version.

root.etag is the quoted root token ready for an assembly update’s If-Match header.

byState gives complete active, archived, and draft counts across returned line occurrences, including zero-valued buckets.

uniqueByState gives the same lifecycle view across distinct child records, without repeated lines inflating risk.

Parts summary (BOM explosion)

GET/entities/{id}/parts-summaryRead

Order-free BOM explosion: every leaf material with its compounded quantity per one unit of the root.

qtyPerUnit compounds consumption alone; qtyGross additionally compounds each line’s wastage percent — the procurement figure (the two are equal when no wastage field is configured, see hasWastage). Add ?asOf=YYYY-MM-DD to restrict the walk to BOM lines effective on that date, and ?asOfUnit= to restrict it to a build / PO / batch ordinal (the two AND). Pass the same lens you pass to structure — exploding on one axis and not the other returns a parts list for a different set of lines than the tree.

qtyFixed is non-scaling demand required once per build and is omitted when zero.

Rows include representative material thumbnailUrl and previewUrl when available.

Readable suppliers likewise include supplierThumbnailUrl and supplierPreviewUrl.

state is no_bom when the record’s type has no structure configured, and multi_output_formula when the composition carries Co-Product or By-Product lines: the walk treats every line as consumed, so exploding one would report an output as a material to buy. No rows come back in that case rather than wrong ones.

rowCount reports the number of returned material rows.

supplierCount reports distinct readable suppliers represented by those rows (zero when supplier access is denied).

unsourcedRowCount reports readable material rows with no supplier assignment; it is zero when supplier data is withheld.

supplierWithheld distinguishes access-policy redaction from genuinely unassigned suppliers.

unitWithheld likewise distinguishes redacted units from unset units.

wastageWithheld distinguishes redaction from no wastage configuration; when true, gross quantities are collapsed safely.

Reuse the response ETag as If-None-Match to receive 304 while the parts summary is unchanged.

shapejson
{ "state": "ready", "hasWastage": true, "rowCount": 1, "supplierCount": 1, "unsourcedRowCount": 0, "supplierWithheld": false, "unitWithheld": false, "wastageWithheld": false,
  "rows": [ { materialId, materialName, materialRef, thumbnailUrl, previewUrl,
              qtyPerUnit, qtyFixed, qtyGross, unit, supplierId, supplierName,
              supplierThumbnailUrl, supplierPreviewUrl } ] }

Rollups

GET/entities/{id}/rollupsRead

Evaluate every numeric rollup the record’s type declares over its BOM (mass, packaging weight, lead time…). Configuring rollups is a dashboard operation.

A sum rollup is Σ(child value × line quantity) over the whole BOM, bottoming out at components with no lines; a node with lines contributes its children’s sum, and its own value for the source field comes back as ownValue — a variance signal, never an input. Line quantity is the raw quantity (wastage inflates cost, not a physical rollup). A critical-path rollup is own + max(child) — quantity deliberately ignored — and carries path[], the gating chain from root to the leaf that drives the total.

leafCount / uncapturedCount report how many components were consulted and how many carry no value, so a total with gaps is never passed off as complete. ?asOf works as on parts-summary. configured is false when the type declares no rollups or has no BOM structure; an unsupportedReason (sheet-level multi_output_formula, per-rollup mixed_line_units) marks a result that was declined rather than computed wrong.

Each metric’s capturedCount reports consulted components that do carry a value.

Each metric’s coveragePct reports captured value coverage from 0–100; a supported metric with no leaves is 100 and a declined metric is 0.

Each metric’s complete is true only when it is supported and has no uncaptured values.

rollupCount reports the number of returned rollup metrics.

supportedRollupCount reports metrics that produced a computed result rather than a per-rollup decline.

unsupportedRollupCount reports metrics declined with an actionable unsupported reason.

completeRollupCount reports supported metrics with no uncaptured component values.

incompleteRollupCount reports supported metrics that still contain one or more value gaps.

uncapturedOccurrenceCount sums metric/component value gaps across the sheet; the same component can count once per metric.

lineOccurrenceCount totals direct-contribution rows across all metrics for workload sizing.

Each metric’s lineCount reports its own direct-contribution row count.

Each metric’s pathStepCount reports its own gating-chain length.

pathStepCount totals gating-chain steps across path-producing metrics.

leafOccurrenceCount sums consulted component occurrences across metrics.

capturedOccurrenceCount sums the subset that carry captured values.

Sheet-level coveragePct is the weighted captured percentage across all consulted occurrences.

uniqueGapEntityCount deduplicates components missing values across multiple metrics.

uniqueLineChildCount deduplicates direct contributors repeated across metrics.

hasGaps is the direct readiness predicate for any uncaptured component value.

hasUnsupported is true for either a sheet-level decline or any declined metric.

ready is true only when the sheet is configured with at least one metric and every result is supported with complete value capture.

Reuse the response ETag as If-None-Match to receive 304 while computed rollups are unchanged.

shapejson
{ "configured": true, "ready": true, "rollupCount": 1, "supportedRollupCount": 1, "unsupportedRollupCount": 0, "completeRollupCount": 1, "incompleteRollupCount": 0, "hasGaps": false, "hasUnsupported": false, "coveragePct": 100, "leafOccurrenceCount": 3, "capturedOccurrenceCount": 3, "uncapturedOccurrenceCount": 0, "uniqueGapEntityCount": 0, "lineOccurrenceCount": 3, "uniqueLineChildCount": 3, "pathStepCount": 0,
  "rollups": [ { name, unit, total, ownValue?, leafCount, capturedCount, coveragePct, complete, uncapturedCount, lineCount, pathStepCount,
                 uncaptured:[ { entityId, name, referenceId } ],
                 lines:[ { childEntityId, childName, childRef, quantity, unitValue, extended } ],
                 path?:[ { entityId, name, referenceId, ownValue, cumulative } ] } ] }

Where-used

GET/entities/{id}/usagesRead

Where-used, both levels of the answer: the parents that directly consume {id}, and the whole upward walk to the top-level items that reach it. Query parameters are rejected. Reuse its ETag with If-None-Match to receive 304 while the upward graph is unchanged.

shapejson
{ "usages":    [ { parent, quantity, junctionId } ],
  "ancestors": [ { parent, consumed, quantity, junctionId, depth, cycleCut } ],
  "roots":     [ { ...parent, sellable, depth, via, viaCount } ],
  "directCount": 3, "directParentCount": 2, "edgeCount": 11, "cycleCutCount": 0,
  "itemCount": 9, "rootCount": 2, "sellableRootCount": 1,
  "maxDepth": 3, "depthLimitReached": false,
  "byState": { "active": 7, "archived": 1, "draft": 1 },
  "rootByState": { "active": 2, "archived": 0, "draft": 0 } }

usages is the direct parents (each with the rendered per-line quantity and its junctionId), with directCount reporting immediate BOM lines, directParentCount reporting unique immediate assemblies, and directByState/directByTypeKey breaking those assemblies down by lifecycle and stable type. ancestors contains every upward edge, summarized by edgeCount, with its depth (1 = direct) and a cycleCut flag where the walk stopped on a repeat; top-level cycleCutCount exposes any such graph anomaly immediately, while depthLimitReached warns when traversal reached the 50-level safety boundary. roots are ancestors nothing further consumes — finished items, flagged sellable when their type is listable and summarized by sellableRootCount. Top-level byState and byTypeKey count distinct ancestors without double-counting repeated edges; rootByState and rootByTypeKey narrow those breakdowns to top-level affected products. Every parent, consumed record, and root includes its stable typeKey plus write-ready version/etag.

Every record in a where-used answer carries state — active, archived or draft. Archived and draft parents are included on purpose: an archived assembly still consumes this record, which is exactly what a caller retiring or substituting it must see. Do not read an unfiltered count as live demand.

GET/entities/{id}/alternate-usagesRead

The reverse for approved alternates: the BOM lines that list {id} as an approved alternate, each naming the parent assembly, its stable parentTypeKey, parentState, write-ready parentVersion/parentEtag, and the primary child with stable primaryChildTypeKey, primaryChildState, and write-ready version/ETag (deduped by junction). Top-level total, byParentTypeKey, byParentState, and byPrimaryChildState counts report the impact scope. Unsupported query parameters return 400. Reuse its ETag with If-None-Match to receive 304 while the alternate-use set is unchanged.

Relationships

GET/entities/{id}/relationshipsRead

Inbound field-keyed references to {id}, grouped by source field. Each referrer includes its stable typeKey and write-ready version/etag. Top-level and per-group total, shown, and truncated values expose exactly where sampling occurred; groupCount summarizes fields and shownByState breaks returned samples into active, archived, and draft records. Reuse its ETag with If-None-Match to receive 304 while the relationship graph is unchanged.

Each group carries fieldKey, fieldLabel, total, and a shown sample of referrers. Each referrer carries a state (active / archived / draft) — as with where-used, archived and draft referrers are listed deliberately, so distinguish them rather than treating every row as live. BOM junction fields are excluded (use structure / usages for those). Outbound references are just the record’s own ref / collection field values.

Authoring BOM lines

POST/entities/{id}/composition/linesWrite

Append a component: { "childId": "...", "quantity": "2", "fields": { "<key>": "<value>" } }. Requires a structure configured for the parent’s type; self-reference and cycles are rejected (400).

PUT/entities/{id}/composition/lines/{lineId}Write

Edit a line (supplied fields merge; an empty string clears a line field). Line writes return an ETag — send it back as If-Match to fail with 412 if the line changed since you read it.

DELETE/entities/{id}/composition/lines/{lineId}Write

Remove a line. Honours If-Match the same way.

POST/entities/{id}/composition/lines/bulkWrite

Atomically add, update, move, and delete up to 200 lines in one ordered request. Add/update can identify a child by id or by { "type": "part", "referenceId": "P-100" }; move takes a zero-based position.

Quote the structure body version from GET /entities/{id}/structure as the batch If-Match. An update, move, or delete may also include its line ETag as ifMatch. Any failed item rolls back every successful item; the HTTP response remains ordered, so require applied:true and failed:0 before treating the command as committed. With preview:true, the same validation and governance checks run but no changes persist; successful item statuses are would_add, would_update, would_move, or would_delete. The endpoint supports working-copy ids for governed changes and refuses direct edits to protected live records under the normal freeze/change-request rules.

preview an atomic BOM editbash
curl -X POST "$BASE/entities/$PARENT/composition/lines/bulk" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -H "If-Match: "$STRUCTURE_VERSION"" \
  -H "Idempotency-Key: $COMMAND_ID" \
  -d '{ "preview": true, "operations": [
    { "op": "add", "child": { "type": "part", "referenceId": "P-100" }, "quantity": "4", "position": 0 },
    { "op": "move", "lineId": "<line-id>", "position": 1 },
    { "op": "delete", "lineId": "<obsolete-line-id>", "ifMatch": "\"<line-version>\"" }
  ] }'
POST/entities/{id}/composition/lines/{lineId}/promote-alternateWrite

Swap the line’s primary child with one of its approved alternates: { "alternateId": "..." }. The displaced primary drops back into the alternates list. A direct edit, not a change request: records a quiet revision on a revision-controlled parent; 409 cr_locked if an open change request locks the parent, 409 already_in_bom if the alternate already has its own line under the same parent.

add a component linebash
curl -X POST "$BASE/entities/$PARENT/composition/lines" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{ "childId": "<part-id>", "quantity": "4",
        "fields": { "reference_designator": "J1" } }'

One component, many parents

POST/entities/bulk-add-composition-lineWrite

Add one component to the primary BOM of many records at once: { "parentIds": [...], "childId": "...", "quantity": "2"? }, at most 200 parents.

Per parent, not wholesale: a parent that cannot take the line is skipped and reported with a skipReason rather than failing the call, so one bad target does not sink the rest. The reasons are missing (no such record), no_bom (its type has no structure configured), duplicate (it already lists the component), cycle (the add would make the BOM circular), cr_locked (an open change request locks it), frozen, archived, draft, and staged (it is a change request’s working copy, not a record). childId must be an accepted component type of every non-skipped parent’s structure, or the call is rejected (400 child_type). quantity is a decimal string and must be greater than zero; omitted leaves the line without an explicit quantity, which reads as 1 downstream. A direct edit, not a change request: it records a quiet revision on revision-controlled parents. Pass ?preview=1 to run the whole operation and roll it back.

what was added, and why the rest were notjson
{ "added": 7, "revisionControlledParents": 3,
  "parents": [ { id, name, referenceId, revisionControlled, added, skipReason? } ] }

Defining a structure (which type is a BOM, its junction / quantity fields) is a dashboard operation. The API authors lines against an already-configured structure.

Substitution

POST/entities/{id}/substituteWrite

Replace this component with another across the BOMs that consume it.

Body { "replacementId": "...", "parentIds": [...]? } — replacementId must be the same entity type; parentIds scopes the swap to a chosen set of parents (omitted or empty = every affected parent; ids outside the affected set are ignored). Every consuming BOM line is repointed, merging quantities where a parent already lists the replacement. A direct edit, not a change request: it records a quiet revision on revision-controlled parents and skips parents locked by an open change request (reported in crLockedSkipped). Pass ?preview=1 to run the whole operation and roll it back — the returned counts equal a real run’s without mutating anything.

blast radius (preview and apply)json
{ "affectedLines": 12, "affectedParents": 5, "mergedLines": 2,
  "revisionControlledParents": 3, "crLockedSkipped": 1,
  "parents": [ { id, name, referenceId, revisionControlled, lines, mergedLines } ] }

Component-wide edits

POST/entities/{id}/composition/bulk-editWrite

Remove this component from every BOM that uses it, or set its quantity there.

{id} is the component, and the blast radius is the one substitute acts on: every BOM line whose child is {id}. These are the two verbs substitution cannot express. Body { "op": "remove" | "set_quantity", "quantity": "2"?, "parentIds": [...]? } — quantity is a decimal string, required for set_quantity, ignored for remove, and must be greater than zero (400 non_positive); parentIds scopes the edit the same way substitution does (omitted or empty = every affected parent; ids outside the affected set are ignored). Pass ?preview=1 to run the whole operation and roll it back.

A direct edit, not a change request: it records a quiet revision on revision-controlled parents, and skips — per parent, leaving the rest of the run to proceed — parents locked by an open change request (crLockedSkipped), archived and draft parents (nonLiveSkipped), and a formula’s output lines, where the component is produced rather than consumed (outputLinesSkipped). A frozen parent is the one exception: it refuses the whole call with 409 entity_locked and nothing is applied, so a run either clears every freeze or changes nothing.

blast radius (preview and apply)json
{ "op": "set_quantity", "affectedLines": 9, "affectedParents": 4,
  "revisionControlledParents": 2, "crLockedSkipped": 1,
  "nonLiveSkipped": 2, "outputLinesSkipped": 0,
  "parents": [ { id, name, referenceId, revisionControlled, lines } ] }

Life limits

GET/entities/{id}/lifeRead

Life remaining on a life-limited unit — accumulated usage against the limit its type declares. Reuse its ETag with If-None-Match to receive 304 while the derived life summary is unchanged.

For a part good for N km / cycles / wears: sums the usage amount of every live record referencing the unit and reports it against the type’s declared limit. The total is derived from the events that produced it, never materialised, so it cannot drift; trashed, archived, draft and change-request records never contribute. pctRemaining is the normalized remaining fraction and becomes negative after overrun. status is ok, warning (at or past 80% consumed) or exceeded; remaining goes negative past the limit — information, not an error. configured is false when the type declares no life limit. Distinct from where-used.

shapejson
{ "configured": true, "limit": 500, "accumulated": 412,
  "remaining": 88, "pctUsed": 0.824, "pctRemaining": 0.176,
  "eventCount": 37, "status": "warning" }

Service due

GET/entities/{id}/service-dueRead

When this record is next due for service, derived from its interval and latest live service visit. Reuse its ETag with If-None-Match to receive 304 while the derived schedule is unchanged.

status is ok, warning (at least 80% of the interval elapsed), due, or unknown. A never-serviced record is unknown, not due. daysUntilDue becomes negative after the date passes; when status is due, overdueDays is the non-negative operational value (zero means due today). configured: false means the type declares no service rule (or its rule fields are unavailable to the caller).

shapejson
{ "configured": true, "intervalDays": 90,
  "lastService": "2026-06-15T00:00:00Z", "dueOn": "2026-09-13T00:00:00Z",
  "daysUntilDue": 13, "visitCount": 4, "status": "warning" }

Compatibility

GET/entities/{id}/compatRead

Whether the record’s installed item appears on its authority’s approved list. Reuse its ETag with If-None-Match to receive 304 while the derived compatibility result is unchanged.

Returns { configured, status, itemId?, authorityId?, missingInputs? }, where status is listed, unlisted, or unknown when either rule input is blank. Resolved IDs let an agent explain or follow the exact pairing without another lookup; ordered missingInputs identifies item and/or authority when unknown. configured: false means no compatibility rule is available. If the type enables compatibility enforcement, a direct create/update that changes the pairing to an unlisted item is rejected with compat_unlisted; a governed change request remains the route through review.

Recall impact

GET/entities/{id}/recall-impactRead

Forward traceability: every lot that consumed {id}, directly or transitively. Reuse its ETag with If-None-Match to receive 304 while the impact graph is unchanged.

Climbs the type’s primary composition plane upward — the inverse of structure. Top-level total, rootCount, maxDepth, and complete byState active/archived/draft counts summarize the incident scope; rootByState narrows those lifecycle counts to finished recall targets. Each hit carries depth (1 = direct) and isRoot — true when nothing further consumes it, i.e. a finished, shippable lot a recall notice acts on. Generic over any type whose primary plane is a self-referential genealogy (a lot consuming lots). Trashed and change-request-staged lots are excluded; archived and draft lots still trace — the safe direction for a recall, since a phased-out lot that shipped must still surface. Empty when the record’s type has no composition plane.

shapejson
{ "total": 3, "rootCount": 1, "maxDepth": 2,
  "byState": { "active": 2, "archived": 1, "draft": 0 },
  "rootByState": { "active": 1, "archived": 0, "draft": 0 },
  "lots": [ { "lot": { ...entity }, "depth": 2, "isRoot": true } ] }

Structure baselines

GET/entities/{id}/structure-baselinesRead

Labeled frozen snapshots of this record’s BOM tree.

GET/entities/{id}/structure-baselines/{baselineId}Read

One baseline, with the full-depth structure captured at freeze time.

A baseline freezes the whole tree, not a pointer to it, so it still answers "what did this look like then" after the components have moved on. Compare a baseline with the live tree — or two baselines with each other — to get the same line-by-line diff structure reads produce.

Revisions

GET/entities/{id}/revisionsRead

The record’s revision history, newest first.

GET/revisions/{id}Read

One revision, with its frozen snapshot.

Every approved change welds an immutable revision recording what the record was, who signed it off and when. Revisions outlive the change request that produced them — an approved request is cleaned up, its revision is the history. Paged with before + limit.

Release & configuration traceability

These reads expose immutable release evidence and configuration provenance to integrations. API keys can inspect readiness and history; issuing certificates, capturing configuration releases/checkpoints/incorporations, and changing the release-controlled type perimeter remain admin-dashboard actions. Schema managers with data-write permission can configure the separate certificate release requirements through REST/MCP.

To preserve a historical certificate or captured configuration as a downloadable, independently verifiable package, use Evidence. Creating or publishing a package is separate from issuing the original certificate or configuration release.

GET/entities/{id}/release-readinessRead

{ releaseControlled, ready, certifiable, version, latestRevision?, currentCertificate?, certificates, blockers }. Proves whether the latest approval-bound revision has a current release certificate. Each blocker has a code and message. An open_change_request blocker includes changeRequestId; an open_nonconformance blocker includes ncrId; a fmea_pending blocker includes fmeaId. Control plan blockers include entityId and, when a plan exists, controlPlanId. Inspection blockers include planId, entityId, and revisionId when available.

GET/entities/{id}/configuration-readinessRead

{ applicable, configured, ready, capturable, version, structureHash?, rootCertificate?, currentRelease?, releases, components, blockers }. Resolves the full-depth BOM and pins every release-controlled descendant to its approved revision certificate.

GET/entities/{id}/as-built-configurationsRead

{ configurations: [...] }. Newest-first immutable provenance linking an instantiated record to the exact released source configuration and baseline used to build it.

GET/entities/{id}/as-maintained-readinessRead

{ applicable, drifted, capturable, version, anchorBaselineId?, anchorEventId?, events, blockers }. Compares the current direct composition with its latest as-built or as-maintained checkpoint.

GET/entities/{id}/configuration-incorporationsRead

{ incorporations: [...] }. Newest-first immutable history of released source configurations adopted after the initial build, with before/after release and baseline ids, effective date, reason, and recorder.

GET/entities/{id}/configuration-incorporation-readiness?releaseId={uuid}Read

Compares the currently adopted configuration with a target release: { capturable, version, currentConfigurationReleaseId, currentReleaseRevNumber, targetConfigurationReleaseId, targetReleaseRevNumber, sourceEntityId, anchorBaselineId, drifted, blockers }. releaseId is required.

Readiness responses return stable blockers[].code values and advisory messages. Branch on the code. Responses carrying version also return that decision version in the ETag header.

Controlled documents

Every project includes a builtin_document record type for specifications, drawings, certificates, test reports, and work instructions. A document revision wraps a managed File record with a stable document number, revision label, owner, effective date, periodic-review date, notes, and workflow-owned status.

Draft → scheduled or released → obsolete

Create and edit a draft-status record with normal record CRUD (omit the generic draft:true option), then submit it to one or more named approvers. The submitted snapshot is read-only until a reviewer requests changes. Final approval freezes it permanently. A future builtin_document_effective_on schedules the revision while its predecessor remains current; automatic activation on that UTC calendar date releases it and atomically retires the predecessor. An absent, current, or past date releases immediately. Watchers are notified when a scheduled revision becomes effective. Cancelling or rescheduling an approved date requires another named approval.

GET/controlled-documents/{id}/revisionsRead

List the complete non-trashed revision family newest-first, including archived obsolete revisions. Each entry exposes its reviewDueOn and derived reviewState; active release, schedule-change, and periodic-review decisions include named-approver progress.

POST/controlled-documents/{id}/revisionsWrite

Create the next revision from a released revision with { "revision": "B" }. The new record starts as an editable draft carrying the source file and metadata.

POST/controlled-documents/{id}/submitWrite

Submit a complete draft with { "requiredApprovers": ["[email protected]"], "classification": "major" }. At least one named workspace member is required. A person approves in the dashboard; final approval applies and freezes the reviewed snapshot. Future-effective revisions enter scheduled; all others release immediately. Use an Idempotency-Key when retrying.

POST/controlled-documents/{id}/schedule-requestsWrite

Submit { "action": "reschedule", "effectiveOn": "2026-11-01", "requiredApprovers": ["[email protected]"] }, or use cancel and omit the date. Approval preserves the frozen audit trail: rescheduling replaces the future date; cancellation archives the never-effective revision as cancelled while its predecessor stays current.

POST/controlled-documents/{id}/review-requestsWrite

Submit { "action": "reaffirm", "nextReviewOn": "2027-11-01", "requiredApprovers": ["[email protected]"] } to preserve the exact released revision and advance its review clock. Use obsolete without a date to retire it. Owners and watchers receive one reminder in the 30-day window and another after the date passes; an overdue review never silently invalidates released evidence.

curl -sS -X POST "$BASE/controlled-documents/$DOC_ID/revisions"   -H "X-API-Key: $KEY" -H "Content-Type: application/json"   -H "Idempotency-Key: revise-DOC-104-B"   -d '{"revision":"B"}'

curl -sS -X POST "$BASE/controlled-documents/$NEXT_DOC_ID/submit"   -H "Authorization: Bearer $MANYROWS_API_KEY"   -H "Content-Type: application/json"   -H "Idempotency-Key: submit-DOC-104-B"   -d '{"requiredApprovers":["[email protected]"],"classification":"major"}'

curl -sS -X POST "$BASE/controlled-documents/$NEXT_DOC_ID/schedule-requests"   -H "Authorization: Bearer $MANYROWS_API_KEY"   -H "Content-Type: application/json"   -d '{"action":"reschedule","effectiveOn":"2026-11-01","requiredApprovers":["[email protected]"]}'

curl -sS -X POST "$BASE/controlled-documents/$DOC_ID/review-requests"   -H "Authorization: Bearer $MANYROWS_API_KEY"   -H "Content-Type: application/json"   -d '{"action":"reaffirm","nextReviewOn":"2027-11-01","requiredApprovers":["[email protected]"]}'

Periodic-review reminders track delivery separately for each recipient. Failed deliveries are retried while completed recipients are skipped; an email retry does not add another inbox notification. Deadline email preferences apply. Email delivery is at least once: a crash after the mail provider accepts a message can cause a repeated email.

Read and search document text

GET/entities/{id}/document-textRead

Read bounded text passages from one exact controlled-document revision's managed PDF or UTF-8 plain-text file. Use the document revision's entity ID, not the managed File ID. The file bytes are checked against the stored SHA-256; document identity and file field permissions are checked on every request.

Optional q is a case-insensitive fragment of up to 200 characters; omit it to traverse nonblank lines. limit defaults to 20 and accepts 1–20 passages. Each passage contains location, text, and citation, with line numbers and PDF page numbers. Long lines are clipped to a 500-character snippet with ellipses; this is not a full-file download.

The response identifies documentId, documentNumber, revision, and, when available, fileId and sha256. Check status before using passages. For a ready result, total counts all matching lines and truncated means more matching lines remain after this response. Pass the returned nextCursor as cursor with the same document and query until no cursor is returned.

Continue matching passageshttp
GET /entities/<documentRevisionId>/document-text?q=inspection&limit=20
# Process passages; retain nextCursor when present.
GET /entities/<documentRevisionId>/document-text?q=inspection&limit=20&cursor=<URL-encoded-nextCursor>

Keep the cursor opaque. A malformed cursor or a different document/query returns 400. A changed managed file identity returns 409 stale_cursor: discard the cursor and restart retrieval. Permission denial returns 403 even on continuation.

A 200 response can instead report an extraction status with no passages: no_file, file_unavailable, unverified_file, unsupported_format, unsupported_encoding, too_large, storage_unavailable, hash_mismatch, no_text, extractor_unavailable, or extraction_failed. Consult message for details. Scanned PDFs have no OCR fallback; empty extraction is not evidence that a document contains no relevant information.

Use GET /types/builtin_document to discover the exact fields. The workflow owns builtin_document_status and builtin_document_previous; generic create/import/update cannot promote status or rewrite released lineage.

Material composition

Open a product or material record’s Material composition tab to enter constituent mass percentages and the consumption unit. Mass units determine their own mass; length, volume and count units need a measured mass per unit. Incomplete composition remains visible as a gap.

GET/entities/{id}/material-compositionRead

Read the record’s nullable structured composition profile and content version.

PUT/entities/{id}/material-compositionWrite

Replace the full profile with { "profile": { "quantityUnit": "m", "massPerUnit": "200", "massUnit": "g", "constituents": [{ "name": "Cotton", "percent": "95" }, { "name": "Elastane", "percent": "5" }] } }. Explicit profile:null removes it. Percentages are mass fractions; their sum cannot exceed 100. Governed records use draft change-request working copies.

GET/entities/{id}/composition-summaryRead

Calculate the effective primary BOM’s contained mass and weighted constituents, with source profiles, revision provenance, missing-data issues and coverage. Optional asOf and asOfUnit select the BOM effectivity lens. Purchasing wastage is excluded. Missing mass never becomes zero, and unsupported formula inputs prevent a complete total. Reading derived evidence requires access to its BOM input fields.

Approved revisions preserve the profile and calculated source evidence. Later material edits leave those captured values intact. Saved cost and substitution comparisons in the workspace can also retain composition evidence.

Nutrition evidence

Open Material composition → Nutrition evidence on each input material. Enter supplier facts per 100 g for energy (kcal), protein, fat, carbohydrate and sugars (g), and sodium (mg). An explicit zero is a measured fact; an empty value remains unknown. Name a common review scope, cite the supplier or review source, and enter a review date before marking the facts reviewed. On the finished formula, add its reviewed serving mass in grams. Existing CPG calorie fields are not treated as reviewed source facts.

The summary expands effective primary formula input leaves, uses their exact physical input masses, and calculates mass-weighted values per 100 g. A reviewed serving mass produces separate per-serving values. A material mass profile supplies the mass basis for each leaf; constituent percentages may remain unclassified without blocking this estimate. Source facts, serving mass and process evidence share a versioned profile independently of the material profile. A formula's own per-100-g facts never override its expanded inputs.

For a single-output formula, optionally add a separate processing review on the finished record. Enter finished-mass yield above 0% and at most 100%, plus an explicit 0–100% retention for each of the six nutrients. Cite the process study or batch evidence and review date. When all source inputs and process assumptions are reviewed, the summary keeps the input-mass estimates and shows distinct processed values per 100 g finished mass and, with a reviewed serving mass, per serving. For example, 20 g protein per 100 g input, 80% finished-mass yield and 50% protein retention gives a processed estimate of 12.5 g protein per 100 g finished mass. Missing retentions remain unknown; the system never assumes 100% retention.

Coverage and scope

Missing or unreviewed source nutrients, missing mass, mixed review scopes, unavailable records or usage lines, invalid quantities, unsupported roles or quantity bases, cycles and exceeded structure limits keep the calculation incomplete. Zero-quantity paths and purchasing waste are excluded. Multi-output formulas and Fixed additions require a separate allocation or mass basis; no result is presented as complete for them. Processed results require a single-output formula, complete reviewed inputs, reviewed mass yield and all six retention percentages. A nutrient mass greater than finished mass, combined protein/fat/carbohydrate above finished mass, or sugars above carbohydrate blocks completeness. The serving result additionally requires a reviewed serving mass. These are estimates, not a regulatory nutrition label: co-product allocation, label rounding and market requirements need separate review.

Approved revisions retain source facts, reviewed process assumptions, versions, mass contributions, gaps and both input and processed calculations. Later supplier edits do not rewrite a frozen revision. Revision comparison shows captured versus current evidence and identifies older revisions that predate nutrition capture. Reversion restores this record's own captured facts and process evidence or their removal; current formula sources are then recalculated. Nutrition-only edits follow change-request review, and specification duplication copies the current profile when copySizeSpec is enabled.

To review proposed package values, open Material composition → Nutrition label reviews on an active, unlocked finished product. The latest approved product revision must contain a complete processed estimate. Choose a market and cite the exact rule edition or source used for manual rounding. Select per 100 g of finished product or, when a reviewed serving mass exists, per serving; enter all six intended display values, including zeroes. Compare them with the frozen processed estimate, add a review note, and record the review. Each immutable review retains the displayed values, reviewer, rule citation, approved revision and its calculation/source evidence. History flags a newer product revision without rewriting prior reviews. A reviewer must assess market-specific rounding, required nutrients, units and claim rules; this workflow does not certify regulatory compliance or create artwork.

GET/entities/{id}/nutrient-factsRead

Read the nullable profile, sequence-bound version and ETag. If-None-Match supports conditional reads. No query parameters are accepted.

PUT/entities/{id}/nutrient-factsRead + write

Replace the full profile with { "profile": { "scope": "Supplier specification", "nutrients": { "energyKcal": "100", "proteinG": "20", "fatG": "0", "carbohydrateG": "0", "sugarsG": "0", "sodiumMg": "0" }, "reviewed": true, "source": "Specification R4", "reviewedOn": "2026-10-04" } } and the exact quoted ETag in If-Match. A serving-only profile may omit nutrients and include servingGrams. Explicit null removes current facts but retains history. Missing validator 428; stale, weak, wildcard or multi-token validator 412. Frozen or submitted records refuse edits; governed live records require a working copy.

GET/entities/{id}/nutrient-facts/historyRead

Read the newest 100 immutable versions including removals, with hasMore for older history. Restored content receives a new sequence.

GET/entities/{id}/nutrition-summaryRead

Read per-100-g and optional per-serving values, source versions and matching approved revision IDs, exact decimal mass, reviewed scope and explicit gaps. Only asOf and asOfUnit select effective usage lines; facts remain current. Project data read permission and all captured BOM input-field permissions protect live and frozen results. All four operations are external REST/MCP tools.

The editor preserves drafts on stale writes and reconciles a lost response only when the exact next version has matching content. Until an uncertain save is verified, calculated results remain hidden. Reads are private and no-store. The small versioned fact table lives in the shared manyrows schema with workspace/project isolation; high-volume record attributes and physical BOM data stay in workspace partitions.

Label review preview and history use dashboard-only GET /entities/{id}/nutrition-label-preview and GET /entities/{id}/nutrition-label-reviews. Signed-in data writers record with POST /entities/{id}/nutrition-label-reviews, a command UUID, approved revision ID, market, rule reference, basis, six display values, and the exact preview ETag in If-Match. Missing ETag returns 428; a changed approved preview returns 412; a reused command with different content returns 409. Exact retries return the original receipt. Read access to all current and captured BOM input fields protects preview and the full history, including off-page records. Reviews are stored in a shared manyrows table and are excluded from external REST/MCP tools.

Allergen evidence

Open an input material’s Material composition → Allergen evidence and choose Edit declaration. Name the review scope, enter declared allergens and cross-contact names separately (one per line), and record the supplier or review source. Mark the source reviewed only after checking both lists and entering its review date. Use consistent names and the same review scope across inputs. Existing allergen notes and multiselect fields are not automatically treated as reviewed declarations.

Empty lists remain unknown until the source is explicitly reviewed. Reviewed empty lists mean none declared within the named scope. The derived view traces effective primary formula input leaves, preserving every declaration’s content, version and source. Expand an input to inspect its declaration or open the input record. Shared inputs appear once; zero-quantity branches are excluded. Positive fixed additions can contribute qualitative evidence without a measured mass. An assembly’s own declaration cannot override its expanded inputs.

Coverage gaps stay visible

Missing or unreviewed declarations, mixed scopes, negative quantities, unsupported roles or bases, unavailable inputs or usage lines, cycles and exceeded evaluation limits keep the result incomplete. A deleted input behind a live usage line remains a gap. Formula outputs are excluded as inputs; formulas with outputs retain known input evidence but remain incomplete because output separation is unsupported. Evaluation supports up to 50 levels, 10,000 usage lines, 1,000 distinct input leaves and 1,000 distinct names. Sources and gaps appear in batches of 50. Effectivity selects formula lines; source declarations remain current.

Approved revisions retain the evaluated declarations and coverage, including gaps. Revision comparison uses that captured evidence without looking up today’s suppliers. Older revisions disclose that allergen evidence was not captured. Reversion restores this record’s captured declaration or removal; the formula then evaluates its current input declarations, so it does not roll back every supplier’s evidence. Declaration-only edits follow the normal change-request workflow. Specification duplication copies current declarations when copySizeSpec is enabled, preserving the original record’s history.

GET/entities/{id}/allergen-declarationRead

Read the nullable declaration, sequence number and ETag. If-None-Match supports conditional reads. No query parameters are accepted.

PUT/entities/{id}/allergen-declarationRead + write

Replace the full declaration with { "profile": { "scope": "Supplier review scope", "contains": ["Milk"], "crossContact": ["Peanut"], "reviewed": true, "source": "Supplier statement R4", "reviewedOn": "2026-10-04" } } and its exact quoted ETag in If-Match. Both arrays are required; explicit { "profile": null } removes the declaration and retains a removal version. At most 100 unique names across both lists (80 characters per name), a 120-character scope and a 2,000-character source reference. Missing tokens return 428; stale, weak, wildcard or multi-token headers return 412. Identical normalized content creates no new version or audit entry. Frozen/submitted records refuse edits; governed live records require a working copy.

GET/entities/{id}/allergen-declaration/historyRead

Read the latest 100 immutable declaration versions, newest first, including removals. hasMore signals older versions. No query parameters are accepted. Restoring old content receives a new sequence-bound token.

GET/entities/{id}/allergen-summaryRead

Return separate declared and cross-contact name unions, contributing source IDs, complete source declarations, matching latest approved source revision IDs when available, and explicit coverage issues. Optional asOf and asOfUnit select effective usage lines. Other query parameters are refused. Project read access and every parent, child, quantity, role and basis input-field permission are required; captured field IDs continue protecting frozen evidence after mapping changes. All four operations are available through external REST and MCP.

A stale edit preserves the draft for copying and blocks blind retries. After a lost save response, the editor reads the exact next version and reconciles matching content. If the outcome cannot be verified, calculated evidence is hidden until a successful reload. Closing or navigating scopes away pending UI work; inspect current declaration history before starting a new edit. Fresh read denials clear cached declarations, source details and history. Reads use private, no-store responses.

Complete means reviewed input coverage in a common team-maintained scope. Prepare finished-product labels only after reviewing the applicable scope, supplier evidence, processing and manufacturing cross-contact. No regulatory allergen vocabulary, alias matching, concentration threshold, automatic expiry assessment, processing-loss calculation or label generation is supplied.

Market constituent checks

Open a record’s Material composition → Market constituent checks. A schema administrator with data-write access can create a team-owned rule set with a stable key, name, market, edition, responsible team or provider, source reference and applicable record types. Enter each constituent’s maximum mass percentage explicitly. Zero requires absence; equality with a maximum passes. Each set supports 1–50 types and 1–50 unique constituent names, with maxima from 0 to 100 and up to 12 decimal places.

Select a rule set and edition, then check Current live composition or the Latest approved revision. Live checks use the current BOM effectivity lens. Approved checks use only the composition captured in that revision. The server compares exact mass-percentage ratios, so a displayed rounded value can match a threshold while its exact ratio still exceeds it. Complete composition is required: missing mass, unclassified content, unsupported nested Fixed additions or multi-output formulas remain Unknown. Older approved snapshots without exact percentage evidence remain unknown for present constituents until the composition is reviewed in a new revision.

Record approved check saves the reviewed result for the latest approved revision of an active, unfrozen live record. Data-write access is required. It retains the rule edition, responsible team, source, composition, captured input fields and exact ratios. A new rule version or material edit leaves saved evidence intact; a newer target revision adds a notice. Recorded checks can preserve exceeded limits and unknown results as well as passing results. Export CSV keeps source identities, limits, exact ratios, display estimates and reasons.

New rule version preserves earlier editions and checks. Concurrent edits require the current preceding version. Reload rechecks the current rules, history and permissions; denied reads hide saved figures and exports. Current BOM field permissions and all captured fields protect the entire history, including entries outside the page. Lists use 50-row pages.

If a save response is lost, keep the dialog open and use Retry to send the same command, references and reviewed validator. An exact retry returns the original receipt even after a newer revision or rule edition exists, with current permissions still enforced. Pending commands last only while their dialog and record context remain open. Before closing an uncertain request or starting a fresh one, reload and check recorded history; for a rule version, reload the rule catalog. A refused stale preview or head requires closing the editor, reloading and reviewing the current evidence.

These are team-maintained constituent limits. Market checks do not supply country regulations, automatically approve rule content, authorize release, calculate nutrition or allergens, or generate regulatory labels. For qualitative input declarations, use the separate Allergen evidence workflow. Market checks are dashboard-only; their routes are excluded from external REST and MCP tools.

Environmental impact

Open Environmental impact on a product or material record. Enter a material-production factor in kg CO₂e per kg, its calculation method or GWP convention, source and dataset version. The supported boundary is cradle_to_gate. Source URL, region and notes are optional; explicit zero is valid.

GET/entities/{id}/material-impactRead

Read { profile, version, versionNumber, createdBy?, createdAt? }. The ETag accepts If-None-Match; unchanged versions return 304.

PUT/entities/{id}/material-impactWrite

Send the complete profile and its current quoted version in If-Match. Missing validators return 428; stale versions and wildcards return 412. Explicit null appends a removal. Unchanged content creates no extra history; restoring an earlier value creates a new version. Changes require data-write access and follow entity freezes and change-request governance.

curl -sS -X PUT "$BASE/entities/$MATERIAL_ID/material-impact" \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -H "If-Match: $FACTOR_ETAG" \
  -d '{"profile":{"kgCO2ePerKg":"2.1","boundary":"cradle_to_gate","method":"IPCC AR6 GWP100","source":"Supplier LCA","sourceVersion":"2026"}}'
GET/entities/{id}/material-impact/historyRead

Read the latest 100 immutable versions, including removals. hasMore discloses older history outside this response.

GET/entities/{id}/material-impact-summaryRead

Estimate contained-material production CO₂e from the primary BOM’s mass and leaf-material factors. Optional asOf and asOfUnit use the same composition lens. Responses preserve physical profiles, factor versions, source contributions and issues. Missing mass or factors leave the total absent; known contributions remain partial. Different method labels prevent aggregate CO₂e. Coverage measures covered mass against total contained mass and is absent when total mass is unknown.

Revision evidence and saved substitution comparisons retain the evaluated inputs. A factor change makes an earlier comparison stale. This estimate covers contained-material production; manufacturing losses, assembly, transport, use and end of life need separate accounting. It is not a verified product lifecycle assessment.

Certificate applicability

Use existing certificate records from your project model. Enable change-request governance and approve the certificate’s values, then approve the product or material revision it supports. On the target’s Certificates tab, choose Link certificate, inspect the captured certificate revision, enter its supported scope and inclusive validity dates, and confirm the scope attestation.

A link applies to the two exact approved revisions. A newer certificate revision, a newer target revision, expiry, an unavailable certificate record or withdrawal makes the old receipt inapplicable to current heads. Its original identity, scope, dates and evidence remain in history. Material certificates do not automatically certify parent products, qualify suppliers or authorize release; these are separate decisions. Dates and scope are supplied explicitly, rather than inferred from configurable certificate fields.

GET/entities/{id}/certificate-applicabilityRead

Returns { entries, total, limit, offset, hasMore, assessedOn, latestRevision?, canWrite } from one repeatable-read snapshot. Pages default to 50; the maximum is 200. Optional revisionId selects receipts for an exact revision of this target. Assessment uses today’s UTC date, current revision heads and withdrawal state; historical filtering does not reconstruct past applicability. Responses are private, no-store. Unsupported query parameters return 400.

Each entry contains the target revision, certificate revision, captured certificate name and reference, declared scope and dates, actor, version, applicability issues and optional withdrawal evidence. Open GET /revisions/{certificateRevisionId} for saved field values with current permissions applied. Custom certificate fields are not duplicated into applicability receipts. Archived or replaced targets and certificates are unavailable. Source deletion preserves captured identity; purging the target removes its entity-owned receipts.

POST/entities/{id}/certificate-applicabilityWrite

Requires data-write permission and each record’s latest approval-bound revision. The target must be active and unfrozen. Working copies, drafts, archived targets and linking a record to itself are refused. Provide a caller-generated UUID id; the supported scope is required (up to 500 characters), as are both dates and scopeConfirmed:true. Optional notes allow up to 2000 characters.

curl -sS -X POST "$BASE/entities/$PRODUCT_ID/certificate-applicability" \
  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
  -d '{"id":"<command UUID>","revisionId":"<approved target revision UUID>","certificateEntityId":"<certificate record UUID>","certificateRevisionId":"<approved certificate revision UUID>","claim":"Organic cotton yarns in this material specification","validFrom":"2026-01-01","validUntil":"2026-12-31","scopeConfirmed":true,"note":"Scope reviewed against the saved specification"}'

A new receipt returns 201 with { id, version, replayed:false }. After an uncertain response, retry the same ID, normalized body and credential. Replay returns 200 without another audit event, even after renewal or withdrawal. Reusing the ID with different content or actor returns 409. Evidence and audit commit together.

POST/entities/{id}/certificate-applicability/{applicabilityId}/withdrawWrite

Append { "reason": "Issuer revoked this certificate" } with the entry’s exact quoted version in If-Match. A nonempty reason is required (up to 2000 characters). Missing validators return 428; mismatches, wildcards and validator lists return 412. Original evidence stays intact. Retry an uncertain withdrawal with its original reason, actor and version; another actor or reason cannot rewrite it. The target freeze and lifecycle rules apply to new withdrawals.

The revision view also lists its scope receipts. View captured certificate revision shows saved visible fields rather than today’s edited values. Read-only users can inspect evidence; writers can withdraw applicability with a reason. An uncertain workspace request preserves its body for an explicit retry, and a stale revision requires reopening the form to review current inputs.

Certificate schemes and scope rules

On a live record’s Certificates tab, project schema managers with data-write access can open Certificate schemes. Define a stable scheme identifier, name and certificate edition; choose certificate record types and three distinct existing fields for the certificate’s scheme, edition and scope identifiers. Scheme and edition fields accept text or single select; scope also accepts multi-select. Approve certificate values containing these identifiers before linking evidence.

Each scope has a stable identifier, a name and allowed product or material record types. Add optional rules for approved target fields: text, single-select and boolean fields support equality; integer, decimal, money and percent fields also support minimum and maximum bounds. Boolean rule values are true or false. Optional constituent rules require a minimum percentage from 0 to 100. They use the saved approved material profile or physical BOM composition. An incomplete configured BOM summary stays unknown; it does not fall back to a manual assembly profile.

Saving creates an immutable definition version. Existing assessments retain their selected rules and field bindings. Choose Create next version to revise a definition; a concurrent save requires reviewing the new head. These are project-defined checks, not built-in regulatory standards or issuer verification. A separate release policy can require passing certificate coverage before certification.

GET/certificate-schemesRead

Returns { versions, total, limit, offset, hasMore, canManage }. Without key, lists the latest definition of each scheme. An exact key selects all its versions, newest first. Pages default to 50 and clamp at 200. Read one exact immutable version with GET /certificate-schemes/{versionId}.

POST/certificate-schemesSchema + Write

Provide a caller-generated UUID id, scheme key, name, edition, certificateTypeIds, schemeFieldId, editionFieldId, scopeFieldId and 1–20 scopes. Each scope supplies key, name and targetTypeIds, plus up to 20 field conditions and 20 constituent minima. The canonical OpenAPI describes the full request shape. Updates require the exact latest previousVersionId; first versions omit it. New saves return 201 with { version, replayed:false }. Exact ID/body/actor retries return 200, including after later versions. Stale heads or reused IDs return 409.

GET/entities/{id}/certificate-scope-assessmentRead

Preview without saving history. Supply exact revisionId, certificateEntityId, certificateRevisionId, schemeVersionId and scopeKey query parameters. Both records must be active at those latest approved revisions. A frozen target can be previewed. Returns pass, gap or unknown, individual reasons and the captured selected rules. A known failed check yields gap; missing or unusable inputs yield unknown when no known gap exists. Observed values are not copied into results.

In Link certificate, choose the optional scheme and structured scope to preview the checks. Use Load earlier definitions to select a saved version for an earlier certificate edition; the same action is available in the coverage filter. Include both schemeVersionId and scopeKey in the applicability creation body to save that assessment with the receipt. Dates, a supported-scope claim and confirmation remain required. Gap and unknown assessments can be recorded for review, but cannot count as usable structured links. Earlier manual attestations remain explicit attestations; they are not mapped into schemes automatically.

All four scheme and preview operations have typed MCP contracts. Reading definitions or assessments requires access to every captured rule input. Saved results also retain the physical composition input IDs used for constituent checks. Hidden source, target or composition inputs return 403 for previews, affected receipt pages and writes. Responses are private, no-store; unsupported query parameters return 400. Keep the exact request for uncertain retries; the editor retains it across closing and reopening.

Certificate release requirements

In Release control policy, save the release-controlled type settings first, then choose Manage certificate requirements. Select a type, an exact scheme definition version, its scope, and the records that must satisfy it. This record checks only the release target and needs no BOM. Selected types in the primary BOM checks every distinct selected type, including matching roots and intermediate assemblies. The selected types must be supported by the scope. BOM checks include all effectivity periods; the live Certificates coverage report’s date/unit lens does not change release requirements.

Each required record needs its own usable structured receipt for the pinned scheme version and scope, linked to its exact latest approved revision. Legacy attestations, other scheme versions, expired or withdrawn receipts, changed certificate heads, and gap or unknown scope assessments do not satisfy the gate. Shared assemblies count once; zero-quantity and formula-output branches are excluded. Missing or invalid BOMs, unknown record revisions, and a selection containing no matching records block certification. Counts describe record evidence, rather than certified mass or issuer verification.

GET/certificate-release-policiesRead

Returns { versions, total, limit, offset, hasMore, canManage }. Without entityTypeId, pages the latest policy for each type. An exact type UUID selects its complete newest-first history. Pages default to 50 and clamp at 200. Read an exact immutable policy with GET /certificate-release-policies/{versionId}. Versions preserve the selected scheme rules, field bindings, type names, author and time.

POST/certificate-release-policiesSchema + Write

Provide a caller-generated UUID id, entityTypeId and an explicit requirements array with up to ten distinct entries. Each entry supplies schemeVersionId, scopeKey, and recordScope (root or bom). BOM entries also select 1–50 distinct targetTypeIds; root entries omit them. First policies omit previousVersionId; updates require the exact current policy head. New saves return 201; the original actor’s identical ID/body retry returns 200. Changed heads or reused commands return 409. Save an explicit empty requirements array to disable this gate for future certifications. Scheme updates do not automatically update policy pins.

GET /entities/{id}/release-readiness returns certificateCoverage and certificate_coverage_gap or certificate_coverage_unknown blockers. The policy, live primary BOM membership, current approved revision heads, all matching receipts, withdrawals and inclusive validity dates are checked in one consistent snapshot using the server’s UTC transaction date. No client date override is accepted. Every selected record is included, with its revision and one usable receipt pin; history pages and the live report’s five-example limit cannot truncate a passing checkpoint. The supported limits are 1,000 selected records per requirement and 4 MiB per checkpoint. Exceeding a limit yields unknown and blocks certification.

Certification saves that policy and coverage checkpoint inside the immutable release certificate. The Changes & releases view shows current requirements until certification, then labels the saved evidence as Certificate evidence at release. Later policies, expiry, withdrawal, source renewals or BOM changes do not rewrite that checkpoint or revoke an issued certificate. New product revisions use the current policy. Older certificates without a checkpoint stay unchanged. Readiness versions cover policy and coverage changes; the confirmation dialog requires another review if its displayed readiness changes. A conflicting record edit during certification also requires a fresh review and saves no release evidence.

Historical release evidence packages include the saved checkpoint in release-certificate.json, without reassessing today’s certificates. Receipt pins do not add source certificate files or full source revision bodies to the package. These checks do not certify shipment eligibility or regulatory compliance.

All three policy operations and release readiness have typed MCP contracts. Policy writes require data read, data write, schema management and visibility of captured rule inputs. Release decisions and history also require all captured scheme, composition and BOM inputs to be readable. Hidden inputs return 403 before a decision or version is exposed, including release briefs and configuration readiness; batch snapshots report a forbidden section. Responses are private, no-store and unsupported query parameters return 400. Preserve the exact command on uncertain saves; the editor keeps it across closing and reopening, and conflicts require reviewing the latest policy.

Sample and test review sessions

Open Quality → Sample & test reviews and choose the physical sample or tested record. Select the product or material specification record and the exact approved change-request revision used in the review. Historical approved revisions are selectable; the review does not silently switch to today’s specification. For apparel or footwear charts, choose the physical sample’s Tested size: measurements use that size’s graded or overridden targets, rather than the base size. Other records use the revision’s named characteristics, units, methods and tolerances.

Enter the measured values, attach up to ten photos using the image uploader or library, add notes, and choose a decision. Use Annotate on an attached photo to place numbered pins and write fit or test instructions. Applying annotations updates the review draft; saving the review captures them with its photos. Blank measurement rows stay unmeasured; missing targets produce an unknown result. Unstated tolerance sides are zero. Sized deviations are rounded to three decimals before comparison, and no size tolerances means unknown. An unsized characteristic with a nominal and no tolerances requires exact equality. Measurement results help the reviewer; they do not automatically choose approved, conditional or rejected.

Use Import measured values to choose a local CSV/TSV file or paste measurement rows. The batch must have exactly two headers, characteristic and value, with at most 200 rows and 256 KiB of UTF-8 text. Comma, semicolon and tab separators and quoted names are supported. Names must match the selected approved revision exactly, including case, after trimming surrounding whitespace; list each name only once. Enter finite decimal numbers in the displayed units, without units or thousands separators. Overflow and nonzero values too small to represent are rejected. Preview values shows the current and imported actuals, captured target and derived result against the selected revision and tested size. Every error must be corrected before Apply to draft is available. Blank values clear only the named rows; omitted rows keep their current values. Applying updates the working draft and its local autosave. It keeps the review decision, notes, photos and follow-up and does not submit a review. Cancel leaves the draft unchanged. Raw file contents, pasted text and unapplied previews are not saved.

A rejected review can Request another sample with instructions, a workspace member owner and a due date. Saving creates a rework action on the same quality round. Open the linked quality round to assign, amend or close today’s action. The session continues to show the original instructions and preserves its sample identity, reviewed revision, size, measurement limits and results, photo names, rendition URLs and annotations, decision, reviewer and timestamp. A subsequent review adds another session. It does not create the next sample record or send a supplier message.

Use Open sample request on a requested review, or open Sample requests, to connect that request to receipt and follow-up review. Record sample receipt selects an existing received sample, part, material or reworked original record and records its actual received date, on or after the original review. It does not create a record or change its lifecycle attributes. The request shows the current quality action and links to the received record and original quality round. The queue defaults to requests awaiting receipt or review; filter explicitly to see saved follow-up reviews. Receipt progress and quality action completion are separate: receiving or reviewing a sample does not close the action.

With delivery access, Sample delivery shows the linked supplier, workspace owner, promised arrival, received and outstanding quantities and overdue status. Use Create sample delivery to record a commitment for the exact originally reviewed record, or Link existing delivery to select a matching commitment from paged search. Creating defaults an empty owner to yourself. Replacing or unlinking a delivery requires the displayed latest link and an explanation; earlier associations stay in Delivery link history. A delivery remains bound to one sample request while its link history exists, including after unlinking. Link notes stay with the request and are not copied to procurement notes. Open delivery workbench manages ordinary receipts, amendments and cancellation independently. Delivery progress does not change sample receipt, review or quality action completion.

In Purchasing → Deliveries, an associated delivery shows Original sample request with its original instructions, reviewed specification, tested size and current sample progress. Open original sample request returns to that exact request; Open follow-up review opens its current saved review when available. Replaced or unlinked deliveries show a historical association warning, while cancelled requests retain their explicit cancelled stage. Use Refresh sample request to recheck progress and access. A purged source removes the association; restricted request inputs hide its details while ordinary delivery receipts remain available.

Record the received sample or reworked record as the delivery receipt’s evidence, then use Use delivery receipt in the sample request. Its record and arrival date are fixed for this handoff; Enter receipt manually switches to ordinary receipt entry. Saving captures the active receipt’s delivery, event, link, quantity and unit without recording another supplier receipt. Superseded or voided receipts and mismatched records/dates are refused for a new handoff. Later delivery corrections do not rewrite an already saved sample receipt or review: correct the sample receipt explicitly when necessary. The handoff retains the original approved specification and tested size for the follow-up review.

Start follow-up review opens the exact received record with the original approved specification revision, tested size and check kind selected. Measurements, photos, notes, decision and corrective instructions start fresh. Choose the actual review decision and date, on or after receipt, and explicitly select another approved specification if needed. Saving captures the predecessor review and receipt link in the new immutable evidence. One review can be saved per receipt. Existing drafts or unresolved review saves on that received record are retained; finish or discard an editable draft before starting a different follow-up. Pending commands remain locked until their result is recovered.

Correct receipt and Withdraw receipt require an explanation and append a new event against the displayed latest receipt. Earlier receipts and linked reviews remain in history. A withdrawal returns the request to awaiting receipt; a corrected receipt can start its own follow-up. An unavailable received record cannot start a new review. If a receipt save loses its response, the exact command is saved on this device for the same account, project and original request. Reopen that request and use Retry saved receipt command; recovery never submits automatically or creates another receipt for the same command. Only definite validation rejection unlocks another attempt. Browser storage clearing or eviction can remove local recovery state. Existing review drafts survive the storage upgrade; older open tabs must reload before editing new receipt-linked drafts.

Use Cancel sample request when the original request should stop. Enter a reason and save the request change. Cancellation removes it from the default outstanding queue and blocks new delivery-link changes, receipts, receipt corrections/withdrawals and linked follow-up reviews. Original instructions, receipt history, saved reviews and the current quality action remain available. Select Request cancelled in the progress filter to find it again. Reopen sample request requires another reason and resumes progress from the existing receipt and review.

Request changes append immutable history with the actor and timestamp, using the displayed latest state. If a response is lost, reopen the same request and use Retry saved request change; it recovers the exact saved command without submitting automatically. Receipt saves, request changes and delivery links share one unresolved command slot per account, project and original request, so finish recovery before making another change. Existing pending receipt and state commands survive the version-3 delivery storage upgrade; older open tabs must reload. Already saved receipt and review commands can recover their original results after cancellation. Unfinished linked review drafts remain on this device, but require reopening the request before a new save.

Open Compare reviews, or use Compare this review in history to choose a baseline. Explicitly select a sample and saved review on each side, including reviews from different physical samples. Load older reviews when needed. The comparison shows captured measurements, decisions, notes, photos, annotations and original follow-up. Share the page URL to retain the selected pairs; access is checked when it is opened. Specification revision changes are flagged. A numeric change is shown only when the specification record, measurement basis, tested size, kind, unit, method, target and tolerances match. Absent or unmeasured rows stay visible without a delta; no unit conversion or overall improvement verdict is inferred.

Working inputs autosave in this browser’s local storage, separately for each account, project and sample. Wait for Draft saved on this device before closing the tab. Reopen the same sample with the same account to restore its exact revision and tested size, raw measurement inputs, date, decision, notes, follow-up, attached photo identities and applied annotations. Current access to the sample, specification and every attached photo is checked before restored inputs are shown and before a new review is submitted. A removed review kind requires an explicit replacement; a newer specification does not replace the selected revision. Use Discard draft and confirm to remove an editable draft from this device.

Drafts use IndexedDB and do not sync between devices or browser profiles. They store authored inputs and selected identities, without caching specification limits, source names or image URLs. Unfinished uploads, unadded photos and unapplied annotation edits are not saved. Clearing browser storage removes local drafts and pending commands. Another tab’s changes are detected: load its saved draft before continuing. Unavailable storage or an unreadable draft blocks submission and offers recovery instead of replacing the saved work.

Open the review page without selecting a sample to see Drafts on this device, or use Saved drafts from an active review. The list pages this account’s unfinished work in the current project and checks current sample, specification and photo access before showing a sample name. Resume draft opens the exact sample and performs the restoration checks again. Recover pending save opens the original command’s recovery flow; it does not submit automatically. Unavailable inputs retain a generic entry so recovery remains reachable without exposing their names or authored contents. Unreadable entries are retained and marked for recovery without guessing their save state. The list contains identity and status summaries, rather than notes, measurements, annotations or command bodies. Same-tab committed changes refresh the list; use Refresh drafts to check changes from other tabs. This is browser-local workspace discovery, with no new REST endpoint or MCP tool.

GET/entities/{id}/review-session-specificationRead

Supply the exact revisionId and optional size. Without size, a chart preview lists available sizes; with size it resolves the captured targets. Returns { target, basis, sizes, size?, characteristics, inputFieldIds }. Only approval-bound project revisions are accepted. Older chart revisions without captured size-field bindings return 422: approve a new specification revision. Unsized revisions refuse a size parameter. Every captured input must be visible.

POST/entities/{id}/review-sessionsWrite

Requires data read/write and a live, non-staged, non-draft, non-archived, non-replaced sample. Held samples permit quality observations. Send a stable caller-generated UUID id, exact revisionId, declared project check kind, explicit YYYY-MM-DD checkedOn, and verdict. Optional inputs are size, notes (4000 characters), measurements (up to 200 unique characteristic names and finite values), and photoIds (up to 10 distinct live project images). Submit measured names and values; the server captures specification limits and photo metadata. Missing or null measured values never become zero.

Optional photoAnnotations is an array of { photoId, callouts: [{ x, y, text }] }. Each group must reference a selected photoIds entry, once per photo, with at most 50 callouts. Supply an explicit callouts array and explicit finite x/y percentages from 0–100; trimmed labels must contain 1–200 characters. Empty groups are omitted during normalization. Callouts are captured in the session’s photos and participate in exact command replay. They do not edit the image library’s current annotations or alter image bytes. Older sessions and unannotated request bodies remain supported; uncertain-command recovery retains the submitted annotations.

Rejected-only requestNextSample, followUp (2000 characters), owner and dueOn create a rework action. A next-sample request requires instructions; the due date is on or after the review date. The round, evidence, action and audit commit together. New commands return 201; an exact normalized ID/body/principal retry returns the original result with 200, even after source changes. New commands bind to the stable signed-in account ID, API key ID, or delegated grant plus underlying account ID. Renaming an email, key or agent client preserves replay and the originally captured reviewer label. A different principal with the same displayed name or email cannot replay the command. Legacy commands keep their exact captured-actor rule; their identity is not automatically backfilled, so changing those original labels can still block legacy replay. Different body or principal with the same UUID returns 409. Actor and principal cannot be supplied in the request body. Before posting, the workspace durably stores the exact command on this device. It survives reloads and tab closure and locks editing and draft discard until explicit Retry saved command resolves the result. Access loss, conflicts, unavailable records and uncertain responses retain the command; only a definite 400/422 validation rejection unlocks its editable draft. Retry still requires the server’s current authorization, and the workspace never submits a restored command automatically. A confirmed result clears only that matching local command.

GET/entities/{id}/review-sessionsRead

Newest-first immutable history with limit (default 50, clamped at 200) and nonnegative offset. Returns { sessions, total, limit, offset, hasMore }. The full sample history’s captured inputs protect counts before paging, including sessions outside the page; denied inputs return 403. Read one exact older session with GET /entities/{id}/review-sessions/{sessionId}, guarded by that session’s inputs. Quality rounds include reviewSessionId when linked to captured evidence.

GET/review-session-comparisonRead

Supply all four exact UUID query parameters: leftSampleId, leftSessionId, rightSampleId and rightSessionId. Returns { left, right, differences, rows } using only the captured sessions. Both samples and both sessions’ captured inputs must be readable in this project before any evidence is returned. Unrelated history does not gate an exact authorized pair; history lists and their counts retain the full-history guard.

Rows contain the union of frozen named characteristics, with optional measured values and explicit difference reasons. delta is right minus left, present only for comparable measured rows with a finite result. A different revision warns without blocking otherwise identical captured criteria. Different context, units, methods, targets or tolerances suppress the affected delta. Missing and explicit-zero limits remain distinct. resultChange reports unchanged, fail → pass or pass → fail only for comparable saved pass/fail results; otherwise it is unknown. The generated read-only MCP tool is get_review_session_comparison.

GET/review-sample-requests

Pages { requests, total, limit, offset, hasMore }. Default stages are awaiting_receipt and awaiting_review; stage=reviewed selects saved follow-up history and stage=cancelled selects cancelled requests. Each request includes the original source review, optional current receipt and review, receipt availability, and the current quality action. Optional state holds the latest explained cancellation/reopening event. All project request inputs, including old receipt versions, state events and linked reviews, must be readable before stage filters or counts. Pages use limit (default 50, clamped at 200) and nonnegative offset.

GET/entities/{id}/review-sessions/{sessionId}/sample-request

Reads current receipt and follow-up progress for one exact requested review. GET /entities/{id}/review-sessions/{sessionId}/sample-receipts pages its immutable receipt history, newest version first. Both require all historical inputs in the request before returning evidence or counts. GET /review-sample-receipts/{receiptId} returns an exact receipt’s context with receiptIsCurrent, preserving old context after corrections. Only a current, available receipt without a saved review can start a new follow-up.

POST/entities/{id}/review-sessions/{sessionId}/sample-receipts

Requires data read/write and a rejected review with requestNextSample=true. Supply command id, live project receivedSampleId, receivedOn and optional notes. Reworked original records are supported. For corrections, supply the latest previousId and explanatory notes (at most 2,000 characters). Omit received record and date to withdraw an active receipt. A stale head returns 422 without writing; refresh before correcting. New events return 201; exact normalized replay by the same stable authenticated principal returns 200 with the original receipt and attribution. Changed body, source or principal returns 409. Captured-input and current credential authorization still apply. Receipt and audit commit together; the quality action and original review remain unchanged.

Include optional receiptId in POST /entities/{id}/review-sessions to link one review of that receipt’s exact received record. Its date must be on or after receipt. The new review captures followUpOf and retains predecessor input permissions even when a different specification is selected. Correcting or withdrawing a receipt does not rewrite a saved review. Source purge removes receipt history and clears its storage pointer; surviving follow-up reviews keep their frozen lineage and exact committed-command recovery. Received record purge preserves captured receipt pins but makes that record unavailable.

GET/entities/{id}/review-sessions/{sessionId}/sample-request-events

Pages { events, total, limit, offset, hasMore }, newest state version first. Requires the exact original project/sample/session and all historical request inputs before evidence or counts. Uses limit (default 50, clamped at 200) and nonnegative offset. Source purge removes this history with its receipts.

POST/entities/{id}/review-sessions/{sessionId}/sample-request-events

Requires current data read/write and an original rejected review with requestNextSample=true. Supply command id, status (cancelled or open) and explanatory notes (1–2,000 characters after trimming). The first state event cancels an untouched request. Every later transition requires previousId to name the latest state event. A stale head or repeated state returns 422 without writing. Cancellation blocks new receipt and linked-review commands; reopening preserves existing progress. Event and audit commit together. Exact normalized UUID/body/source/stable-principal replay returns its original 200 result after later transitions; a new event returns 201 and changed reuse returns 409. Current credential and captured-input authorization still apply. No state change edits the quality action or saved evidence.

GET/entities/{id}/review-sessions/{sessionId}/sample-deliveriesRead + cost

Typed MCP tool get_entities_by_id_review_sessions_by_session_id_sample_deliveries. Pages immutable links newest first as { links, total, limit, offset, hasMore }, with default limit 50, clamped 200, and nonnegative offset. Exact subject and all historical request inputs protect history and counts. Each link captures its source sample/session, prior link, version, selected commitment snapshot or explicit unlink, notes, actor, time and input permissions.

POST/entities/{id}/review-sessions/{sessionId}/sample-deliveriesWrite + cost

Typed MCP tool post_entities_by_id_review_sessions_by_session_id_sample_deliveries. Requires an open requested rejected review, data read/write, cost access, stable principal and all historical request inputs. Send command UUID id and either existing deliveryId or create containing supplierId, title, quantity, unit, promisedOn, owner. Supplier must be live and priceable; quantity must be positive with at most 12 integer and 6 decimal digits; owner must be a workspace member, with blank defaulting to the caller. Creation uses the command UUID as the delivery UUID and the original reviewed record as item. Existing links require that same item, an uncancelled delivery and a promised date on or after the review. Replacements/unlink require exact latest previousId and explanatory notes up to 2000 characters; unlink omits deliveryId and create. Stale/repeated heads return 422. Creation, link, webhook intent and audit commit together. Exact normalized UUID/body/source/principal replay returns the original 200 evidence after later changes or cancellation; new commands return 201 and conflicting reuse 409.

Current request and queue responses add canTrackDelivery and optional delivery with latest link and separately refreshed commitment progress, including recorded dispatch totals, estimated quantity in transit, arrival estimate and the latest active carrier/tracking reference. Delivery evidence retains procurement’s cost permission: data readers without it can still read the sample workflow, but delivery fields and receipt proof are omitted. Optional deliveryReceiptId on sample-receipt POST requires cost access and an active receipt on the current linked delivery with exactly matching receivedSampleId and receivedOn. Its saved deliveryReceipt retains linkId, deliveryId, eventId, quantity and unit, guarded by captured request inputs on exact recovery. It never posts another supplier receipt. GET /supplier-deliveries?itemId=... filters matching commitments before counts and paging. Source purge removes delivery-link and sample-receipt history while procurement deliveries and events remain independent. Uncertain delivery commands use Retry saved delivery command with fresh authorization and no automatic submission.

GET/supplier-deliveries/{deliveryId}/sample-requestRead + cost

Typed MCP tool get_supplier_deliveries_by_deliveryid_sample_request. Requires project data read, cost access and every captured historical input of the associated request. Returns { request?, isCurrent } in a repeatable-read snapshot, with the original review, current sample receipt/review, current quality action and refreshed commitment progress. A real unbound delivery or unavailable source returns isCurrent:false without a request; an unknown or foreign-project delivery returns 404. Historical binding remains discoverable after replacement or unlinking, with isCurrent:false. Cancellation retains the binding and explicit request stage. Restricted inputs return 403 without request names, identities or state, including private inputs on older follow-ups whose received record was later purged. No query parameters are accepted. This read never records a receipt, starts a review, changes a local draft or closes a quality action.

All fifteen operations have typed REST/MCP contracts and return private, no-store. Unsupported queries and request properties are refused. Ordinary check deletion, per-kind check import replacement and generic measurement edits return 409 for captured review rounds; purging the physical sample removes its sessions. Photo identity, URLs and structured callouts are captured, while image asset retention follows the ordinary library lifecycle. Callouts render over the saved photo preview; opening the original rendition does not burn annotations into the image. Session measurements live in review evidence; they do not replace current chart actuals or feed generic check measurement trends. A review does not satisfy an inspection-plan execution binding or certify product release. Supplier dispatch and tracking entries are available in Purchasing → Deliveries. Automatic carrier feeds, offline capture and freehand drawing remain separate scopes.

Certificate expiry and renewals

Open Certificate renewals from the project menu or the project home attention link. Review expiring, expired, unusable and future-dated evidence on live records. Filter by status, expiry horizon, record/certificate/scope search, or one exact record. Each row links to the record’s Certificates tab, where data writers can review and link replacement evidence. The queue itself requires only data-read permission.

A group contains all recorded receipts for one record and exact scheme definition version and scope. Manual attestations group by their exact case-sensitive claim after outer whitespace is trimmed. Another definition version, scope or manual claim cannot substitute. The longest currently usable validity wins, so an active exact replacement clears the old expiry alert even when older expired receipts remain in history. Future evidence stays pending until its inclusive start date and cannot clear today’s gap. Held records stay visible; archived, drafted, trashed, replaced and staged targets are excluded.

GET/certificate-renewalsRead

Typed MCP tool get_certificate_renewals. Returns { assessedOn, horizonDays, throughDate, complete, summary?, groups, total?, limit, offset, hasMore, issues, inputFieldIds }. The default status=attention includes every group except current. Other statuses are all, expiring, expired, unusable, upcoming and current. horizonDays defaults to 30 and accepts 1–365. Optional entityId and schemeVersionId select exact project-owned UUIDs. q searches names, references, claims and captured scope labels with at most 200 characters. Group pages default to 50, clamp at 200 and accept a nonnegative offset.

Assessment uses the server’s UTC transaction date and current approved target and source heads, availability, saved scope checks, withdrawal and inclusive validity dates in one consistent snapshot. Validity through today remains usable; Expiring soon includes an end date on or before today plus the horizon. Without usable coverage, otherwise eligible expired evidence takes precedence over future evidence. Withdrawn receipts, changed revisions and failed or incomplete scope checks cannot supply coverage. Rows retain the selected receipt, captured source revision, exact definition version, usable/recorded receipt counts, current coverage end and any future start date.

summary includes total recorded groups, attention and each status count across the whole search and record/scheme selection before status filtering or paging. total counts groups matching the selected status. Every receipt is considered within the supported selection. More than 10,000 selected receipts returns complete:false with receipt_limit, no summary or total, and no rows. Counts are unknown; narrow to one record or review its Certificates history. Search, status and paging do not bypass this assessment limit.

Every selected receipt’s captured rule and composition input IDs protect counts and conditional reads, including hidden receipts outside the search, status, displayed page or assessment bound. Denied inputs return 403 before an ETag or 304 is exposed. Successful responses are private, no-store; an authorized unchanged representation supports If-None-Match and 304. Unsupported parameters and client assessment dates return 400. Project home uses the same default 30-day summary: restricted inputs omit certificateRenewals, while an unavailable or bounded assessment adds that name to failedSections and sets partial:true.

This queue covers recorded scopes. Check release readiness for required scopes that have never had a receipt, and BOM certificate coverage for a product structure. Renewal evidence is linked explicitly to the current approved revision; it does not transfer to a parent, child or newer revision. Queue reads and later renewals never rewrite or revoke immutable certificate evidence saved at release. Project scope rules do not verify issuer claims, regulatory compliance or shipment eligibility.

BOM certificate coverage

The Certificates tab checks the current record and each distinct assembly or material reachable in its primary BOM. Shared assemblies count once. Each record needs its own usable certificate link to its exact latest approved revision; a parent’s link does not cover its children, and a material’s link does not certify the parent. Counts describe evidence presence, rather than certified mass or regulatory compliance.

Use Exact supported scope to select a declared scope. Matching trims outer whitespace and is case-sensitive. A blank filter accepts any declared scope, so inspect the displayed claims before treating the selection as evidence for a particular scheme. Different claims are not automatically equivalent. The report uses the Components tab’s date and unit lens to select current BOM lines, while certificate assessment uses today’s UTC date, current revision heads, availability and withdrawals.

GET/entities/{id}/certificate-coverageRead

Returns { configured, complete, assessedOn, claim?, schemeVersionId?, scopeKey?, totalRecords, linkedRecords, records, issues, inputFieldIds?, limit, offset, hasMore } from one repeatable-read snapshot. Optional query parameters are asOf, asOfUnit, claim (up to 500 characters), schemeVersionId, scopeKey, limit and offset. A scope key requires an exact scheme version. Record pages default to 50; sizes above 200 are clamped. Counts and graph issues cover the whole selection on every page. Historical revisionId and unsupported query parameters return 400. Responses are private, no-store.

Each record includes its current revision metadata, matching receipt count, usable-link count, explicit gaps and up to five captured receipt examples, with usable links preferred. hasMoreEntries identifies additional receipts. Every matching receipt contributes to the counts, including older usable links beyond the displayed examples. Open the record’s Certificates tab or page GET /entities/{id}/certificate-applicability for complete history. View captured certificate revision opens saved certificate fields with current permissions applied.

Zero-quantity branches and formula output branches are excluded; an unset quantity defaults to one. Negative quantities and unknown formula roles keep their records visible and flag the report. Unavailable usage records, cycles and a BOM deeper than 50 levels also prevent a complete result. A complete result requires a configured primary BOM, no graph issues and a usable matching link on every selected record. Without a primary BOM, the report covers only the current record and remains incomplete.

Use Exact scheme definition and Structured scope to require receipts assessed under a particular saved version. Manual attestations and other versions do not fulfill this filter. Saved gap or unknown assessments never count as usable links. Without a scheme filter, the report accepts any usable link and displays its declared scope and saved assessment.

Reading the report requires project data-read access and access to the BOM’s parent-reference, child-reference, quantity and optional role fields, plus every matching receipt’s captured assessment inputs. Hidden inputs return 403 for the entire report, including when they occur outside the displayed record page or receipt examples. Quantity units and material mass are not needed for basic evidence-presence counts; saved constituent assessments retain their own physical inputs. The report is live and read-only; historical revision views retain their direct scope receipts without displaying a current BOM report as frozen evidence. Release authorization remains a separate decision.

Material planning

Build a net material projection for 1–50 orders with POST /requirements/query. The projection combines exploded requirements with unit-specific on-hand and safety stock, active reservations, and open purchase supply. It is read-only and does not reserve anything, so it is available to read-only credentials.

Use POST /material-plan-runs when the result must become an executable plan. A saved run snapshots the projection and atomically reserves the concrete stock and purchase supply it consumed. Saved runs are private to the authenticated principal: member ownership survives an email change, and API-key calls use the exact credential rather than its non-unique display name while retaining its creating member as owner. List or reopen the caller's plans with GET /material-plan-runs and GET /material-plan-runs/{runId}.

Release a run with POST /material-plan-runs/{runId}/release when its reservations are no longer needed. Release is idempotent, preserves the saved result as history, and prevents later dashboard RFQ handoff from that stale plan. Dashboard RFQ creation accepts at most 50 distinct, unambiguous materials per batch and will not create a second RFQ for the same material and run.

Current planning offsets are available from GET /material-stock. Writers can version-update one position with PUT /material-stock or atomically reconcile 1–200 positions with POST /material-stock/reconciliations; every reconciliation requires a reason, checks each current version, and records immutable before/after evidence. Use GET /material-stock/reconciliations for that history.

Formula batch costing

Open a formula record's Cost tab, choose Compare assumptions under Cost scenarios, then New scenario. For formulas with explicit output lines, enter the batch quantity, review every output and assign cost shares totaling exactly 100%. Preview the current-price baseline beside your scenario, label it and choose Save evaluation. This is a dashboard planning workflow requiring cost access.

Set up the formula: its primary composition needs ingredient and output roles, quantities, and a cost mapping. The CPG formulation starter provides optional Formula role, Output yield factor and Quantity basis fields. Unset roles mean Ingredient, unset basis means Variable and unset output yield means 1. Add explicit Primary, Co-Product or By-Product lines for the items produced. Their recipes and prices never become batch inputs. Output quantities are nominal amounts per parent basis unit and use Variable basis.

State the basis: variable ingredient quantities scale with the batch quantity; root Fixed additions apply once per batch. Ingredient wastage increases consumption. Labor, overhead and flat freight/duty extras remain amounts per parent basis unit, while percentage extras apply to the batch FOB subtotal. Compatible consumption-to-pricing unit conversion must be enabled when explicit ingredient units differ from pricing units. Actual batch consumption selects existing volume-price tiers. Under currency conversion, foreign-currency tiers retain the flat source price; tiers in the reporting currency apply normally. The date and build/batch ordinal select composition effectivity; record cost fields still use current values.

Review expected outputs: nominal line quantity × batch quantity × output yield factor gives expected output in the line's unit. Both comparison columns use your batch quantity and cost shares; the baseline uses recorded yields and the scenario uses any entered yield overrides. Blank overrides retain recorded yields. Yield changes affect expected output and allocated unit cost without adding ingredient cost. Cost shares may be zero but must total exactly 100%. Rounded allocations reconcile to the landed batch total; unit cost estimates round to 12 decimal places. Missing ingredient prices keep the result visibly incomplete.

Batch quantities and individual line quantities must be positive and at most 1 billion, with up to 12 fractional places. Output yields must be greater than zero and at most 1. A batch supports up to 50 root output lines and 10,000 formula lines through 50 levels; a deeper active structure is refused. Landed batch cost must be nonnegative and at most 1 trillion. Nested multi-output formulas and nested Fixed additions require their own production-stage basis and are refused during automatic expansion. For an intermediate item, save a separate batch evaluation and use its output in production-stage costing. Single-product selling-price margins, material replacements, automatic allocation and by-product credits are outside this workflow. Ordinary per-unit cost sheets and requirements continue to decline multi-output formulas.

Keep reviewed history: saving requires the exact preview to remain current. Price, quantity, yield, configuration and relevant revision changes require another preview. Saved results retain their original outputs, yields, allocation and field permissions; later changes mark them stale, and an unavailable current output set does not rewrite history. Revise or refresh rereads current output identities, retains matching entered values and asks for shares on newly added outputs before previewing a new immutable evaluation. CSV export includes the batch basis, output yields and allocation, all ingredient contributions and batch totals. This workflow does not change released products, orders, stock, production records, regulatory rules or labels, and adds no external REST or MCP operation.

Production-stage costing

Evaluate each intermediate production stage as its own formula batch. In a downstream record's Cost scenarios editor, set the consumed component's Component change to Saved batch output. Choose the Formula type, Upstream formula, Saved batch evaluation and matching Consumed output line. For a new downstream batch, first enter its quantity and output shares and preview once to load its consumed input choices, then select stage outputs and preview the changed prices. The component must be an active leaf or purchased input, and the output line must produce that exact item. Expanded assemblies still use their normal rollup.

Review the handoff: the source must be a current, fully costed saved scenario with an explicit reporting currency. An incomplete upstream baseline is allowed when its scenario assumptions supply all missing prices. Both stages need the same reporting currency and explicit, supported compatible output and item pricing units; cross-stage FX is not inferred. Enable consumption-to-pricing conversion when explicit downstream line units differ. The baseline retains current pricing, while the scenario uses the saved allocated amount divided by expected output. The server carries that ratio through unit conversion, downstream quantities, nested assembly multiplication and waste before rounding published money. Displayed unit estimates round to 12 places. Explicit zero allocation is a real zero price.

Keep every stage's batch quantity, yield and cost share deliberate. For example, a saved 100 kg-basis batch allocating 200 USD to 80 kg of output prices 500 g consumed downstream at 1.25 USD. Consumption is proportional to the selected output; it does not reserve stock, round up to whole production batches or schedule production. A comparison can reference up to eight upstream production stages and 25 distinct saved evaluations, and a chain cannot revisit a formula. Stage-linked consumption quantities are nonnegative and at most 1 billion, waste is at most 1000%, and landed costs must be nonnegative and at most 1 trillion.

Refresh deliberately: changes to upstream prices, quantities, yields, configuration, relevant revisions or prior stage sources invalidate the handoff. Refresh and save the upstream batch, then explicitly select that new evaluation downstream and preview again. Reload stage rechecks source reads; an inaccessible, incomplete or stale source hides its figures and blocks a new preview. Saving requires the exact reviewed preview version. Existing downstream history retains its original source label, output, batch basis, allocation, expected quantity, yield and version after source edits or deletion; its current result becomes unavailable when that source cannot be re-evaluated. Current upstream configuration and captured field permissions protect previews, headlines, history and deletion, including ancestor stages. These checks continue after an upstream formula record or saved evaluation is deleted.

CSV exports retain source identities, versions, decimal allocation and quantity evidence alongside the comparison totals. These assumptions do not change live item prices, inventory, orders or production data. This remains a dashboard-only planning workflow with no new external REST or MCP operation, automatic formulation optimization, regulatory rules or derived labels.

Supplier quote total-cost comparison

In the dashboard's Costing supplier RFQ workbench, enter a quote's unit-price tiers plus freight, tooling and other one-time charges. Suppliers can enter the same charges through their response link; draft saves, submitted receipts and CSV exports retain them. This is a dashboard and supplier-response workflow, not an external data API operation.

All charges use the RFQ currency and apply once to the requested order, not once per unit or per price tier. Leave a charge blank when unknown; enter 0 when it is included or not applicable. Explain other charges in Notes. Values must be non-negative decimals with up to 18 integer and 6 fractional digits.

The applicable price tier and requested quantity determine the goods subtotal. The complete total is goods subtotal + freight + tooling + other charges. Effective unit cost divides that total by the requested quantity, rounded to six decimal places. MOQ rules still apply: a request below a quote's MOQ does not become eligible merely because its calculated price looks attractive.

At 1,000 units (USD)Unit priceGoods subtotalFreight / tooling / otherComplete totalEffective unit cost
Supplier A4.004,000.00200 / 500 / 04,700.004.70
Supplier B4.504,500.000 / 0 / 04,500.004.50

Supplier B has the higher unit price but the lower complete order cost. Choose Lowest complete total or Highest complete total in the quote sort menu. These rankings compare eligible quotes with all three charges confirmed. Unknown charges are never treated as free: an incomplete quote shows its known subtotal in the detail view, but no complete total or effective unit cost. Such a quote may ultimately cost more or less than a complete quote.

The award dialog shows the cost breakdown and warns when another eligible quote has a lower complete total. Awarding with unknown charges is allowed with the normal rationale; those unknowns are retained in the saved cost basis. New awards preserve the selected quote ID, quantity, currency, unit price, charges and evaluated totals. Existing historic awards are not backfilled with invented cost evidence.

Only unit-price tiers become recurring source prices when awarded. One-time charges are saved as decision evidence, not added to every later unit price. Quote CSVs include charge values, completeness, totals and the award snapshot; imported charge values must agree across every price tier for the same source. Blank CSV charge cells mean unknown, not zero.

Scope: totals include only goods and the three stated charges. Taxes, duties, currency conversion and other unentered costs are not inferred. This comparison does not automatically choose or award a supplier; normal sourcing permissions, quote eligibility and governance checks remain in force.

Recall response cases

Open Nonconformances → Recall response cases → Explore recall cases to turn recorded lot genealogy into accountable containment and recovery work. Create a case with a source lot or record, title, risk and reason, due date, supporting record, owner and independent workspace member reviewer. The reviewer must differ from both creator and owner.

Save the initial scope: creation captures the source plus its ancestors on the source type's primary structure plane in a consistent snapshot. Each affected record receives separate containment and recovery actions, initially assigned to the case owner. The initial trace, original reason and first-seen lot metadata remain saved even if source records change or are later deleted. Supporting-record metadata is copied, not linked document bytes.

Read the limits: the trace is bounded to 20 levels, 200 lots and 2,000 scanned connections, with a 10-second trace timeout. Warnings explicitly identify depth, lot and edge limits, detected cycles, and a missing primary plane. Without a plane, only the source is traced. Archived and draft records are included regardless of effectivity dates; deleted and change-request-staged records are excluded. Other structure planes and external distribution are not searched. A saved trace is not proof of complete genealogy, shipment coverage or physical recovery.

Extend without erasing: the owner can refresh the trace while the case is open or changes have been requested. New affected lots get pending actions; previously affected lots and their work are never removed. The workbench marks retained lots absent from the latest trace and shows the original trace separately. The owner can manually add an affected record with a reason and supporting evidence. The lifetime union is capped at 200 lots: a refresh exceeding that limit is rejected atomically. Coordinate additional cases and document coverage for larger recalls. Pickers show live records; agents can supply known archived/draft source or additional-lot UUIDs.

Execute the response: only each action's assigned owner can record progress or complete it, with a note and supporting record. Progress does not mark completion. For an inapplicable physical action, explicitly justify that conclusion with evidence instead of silently skipping the task. The case owner can reassign pending work or reopen completed work; prior evidence remains in history. The reviewer cannot prepare or own response actions.

Independent closure: once every lot's containment and recovery actions are complete, the case owner submits supporting evidence and an explicit coverage assessment. Address trace warnings, missing genealogy, external distribution and recovery coverage in the assessment. Submission freezes scope and work. Only the assigned independent reviewer can close or request changes. A change request retains completed evidence; the owner can reopen actions, extend scope and resubmit. The owner can cancel any nonterminal case with a reason, including during review. Closed and cancelled cases are terminal. Cancellation does not attest recovery, and closure does not certify regulatory compliance.

The queue loads at most 100 summaries, active first then due date and ID, and warns when partial. Search, filters and summary CSV apply only to the loaded list. Saved ?recallCase=UUID links open cases omitted from the queue. Detail includes searchable, paginated lot work and ordered history, authenticated principal and agent attribution, action CSV and full JSON export. Ordinary source deletion preserves saved cases; explicit project data wipe removes cases and events. This workflow does not automatically alter inventory, shipments, inspections, NCRs or releases, or send customer notifications.

REST and MCP

All five operations are available on the external project Data API and as generated MCP tools. Data-read access is sufficient for reads; writes require data-write access. API keys may own cases and actions as their returned apikey:UUID principal, but cannot impersonate the reviewer. Delegated OAuth MCP retains the underlying assigned member's identity and requires live write permission, write consent and a writable backing key. Capability discovery respects the connection's access mode.

Data API operationMCP tool
GET /recall-casesget_recall_cases
POST /recall-casespost_recall_cases
GET /recall-cases/candidatesget_recall_cases_candidates
GET /recall-cases/{recallCaseId}get_recall_cases_by_recallcaseid
POST /recall-cases/{recallCaseId}/eventspost_recall_cases_by_recallcaseid_events

Every event requires a client-generated UUID id, current version, kind and nonblank note. Send only applicable optional fields: add_lot needs lotId and evidenceId; progress and complete_action need targetId and evidenceId; reopen_action needs targetId; reassign_action needs targetId and owner; submit needs evidenceId with the coverage assessment in its note. Refresh, request changes, close and cancel accept no optional fields.

New saves return 201; unchanged retries by the same principal return 200 with replayed:true, including after later transitions. Retry uncertain results with the original UUID, body and version. Stale new commands and conflicting UUID reuse return 409; refresh before choosing a new action. The server derives actor and principal. MCP arguments group path parameters under path and payloads under body; follow the returned tool schema.

Controlled concessions

Open Nonconformances → Controlled concessions → Explore concessions to request, independently approve and track a bounded exception. This standalone register complements existing “use as is” dispositions; it does not replace or modify them. It never changes inspections, NCR closure, release gates, stock or external customer approvals.

Define the request: choose a live item and an exact recorded revision, identify the affected lot or batch, state the nonconformance or permitted deviation, and describe every condition and required safeguard. Set a quantity limit, unit, expiry date, assigned owner and independent member reviewer, and attach an existing supporting record. Quantities are whole units from 1 to 1,000,000,000; fractions and unit conversion are not supported. Expiry is inclusive through the stated day in UTC and cannot be in the past when a new request is recorded. The UI shows the latest 100 revisions; an older recorded revision ID can be supplied through the API.

The item revision, lot, quantity, unit, deviation, conditions, expiry and assignments are immutable. Before approval, cancel and replace an incorrect request. After approval, the reviewer can withdraw it and a separate request can be made. Each concession has its own allowance: separate approvals for the same lot do not share a global quantity pool. Reviewers must check for overlapping authorizations rather than assume the system allocates physical stock.

Submit and review: the owner submits with supporting evidence and a rationale. The assigned independent reviewer, who must differ from both creator and owner, may approve, reject or request more evidence. A request for more evidence preserves the earlier submission and returns the request to the owner; resubmit with evidence and a new rationale without changing the fixed scope. Approval cannot be recorded after expiry. Rejection is final. The owner may cancel a request before approval, including during review.

Record actual usage: only the assigned owner may record use of an approved, unexpired concession. Supply a positive whole-unit quantity no greater than the remaining allowance, the actual usage date, a unique usage or batch reference, supporting evidence and a note attesting that this item revision, lot and every condition apply. The actual date must be between the approval's UTC day and today and cannot exceed expiry. The server also checks the recording day: backdating a new entry after expiry is rejected. The register enforces recorded quantity and dates; physical applicability and compliance with written safeguards still require verification.

Usage references are case-insensitively unique within one concession, ignoring leading and trailing spaces. They cannot be reused even after an entry is voided. Concurrent saves are serialized and version-checked, so two callers cannot spend the same remaining allowance. The UI shows net usage, remaining quantity, expiry and whether the allowance is exhausted. “Expired” and “Quantity exhausted” are availability indicators derived from the original approval, not replacement decisions or background edits.

Correct or withdraw: the owner can void an incorrect usage entry with a reason and supporting evidence. This corrects a recording error; it is not a physical return. The original entry remains visible, and its quantity is removed from net usage exactly once. Record any replacement separately with a new reference, only if new usage is still permitted. Corrections remain available after expiry or withdrawal, but never reactivate either. Only the assigned reviewer may permanently withdraw an approved concession. Withdrawal stops new use without erasing previous approval or usage.

The queue lists at most 200 concessions, nonterminal first then earliest expiry, and warns when partial. Search, filters and summary CSV apply to the loaded list. Saved ?concession=UUID links still open omitted entries. Detail includes full ordered history with authenticated principal and agent attribution, supporting-record links, voided-entry labels, history CSV and a full JSON export. Original metadata survives source-record deletion, but linked document bytes are not copied. Explicit project data wipe removes the register and its events.

REST and AI-agent access

All five operations are available on the external Data API and as generated MCP tools. Discover actual tool names and schemas through the canonical OpenAPI contract and tools/list. Reads require project data access, not cost access. Writes require a writable credential and the appropriate assigned principal. API-key agents may own requests as their returned stable apikey:<key UUID> identity; a key display name never impersonates a reviewer. Reviewers must be independent workspace members. A member can act through delegated OAuth MCP with live project data-write permission, a writable backing key and project:data:write consent. OAuth is not accepted on the REST Data API.

Method and relative pathOperation ID
GET /concessionsgetConcessions
POST /concessionspostConcessions
GET /concessions/candidatesgetConcessionCandidates
GET /concessions/{concessionId}getConcession
POST /concessions/{concessionId}/eventspostConcessionEvent

Candidate search uses kind=item or kind=evidence and optional q up to 200 characters, with at most 100 results; narrow the query for omitted records. Creation needs a client-generated UUID id. Every event needs a new UUID id, current version, nonblank note and kind. Submit additionally requires evidenceId; use requires quantity, occurredOn, reference and evidenceId; void_use requires targetId of the original usage and evidenceId. Other kinds are approve, reject, request_changes, withdraw and cancel. Omit unrelated fields. Actor and principal are always server-derived.

After an uncertain response, retry the unchanged body with the same UUID and original version. The same authenticated principal and payload replay without consuming quantity again, including after later expiry, correction or withdrawal. Conflicting UUID or usage-reference reuse and stale new commands return 409. Refresh and review before issuing a new command. The usage balance and append-only event are committed atomically.

First-article approval packages

Open Inspection plans → First-article approval → Explore first-article packages to assemble and independently review evidence for a supplier's first samples of a specific item revision. A package is not a production release, supplier-wide qualification, PPAP certification or automatic completion of a supplier change.

Define the package: choose a live price-enabled supplier/source record, a live item and its exact recorded revision, then enter a title, sample lot or build identity, owner, independent member reviewer and review due date. The UI lists the latest 100 revisions; the API also accepts an exact older recorded revision. Define 1–50 requirements with unique keys, inspection kinds and acceptance criteria. Optionally name an exact measured characteristic. Scope, assignments, requirements and dates are immutable: cancel and replace an incorrect package. Supplier and item names are captured as metadata snapshots; the package remains readable after ordinary source deletion.

Attach actual evidence: only the assigned owner can attach or replace an inspection for a requirement. The inspection must be non-deleted, have an approved or rejected verdict, match the package's item, revision and inspection kind, and have a checked date no later than today in UTC. The reviewer cannot be its author. For a named characteristic, a measured value and nominal must exist; tolerance bounds are taken from that recorded measurement, with an absent side treated as zero tolerance. Coverage passes only with an approved verdict and an in-tolerance value. Without a characteristic, coverage uses the inspection verdict. Written acceptance criteria, sampling adequacy and applicable calibration evidence still require reviewer judgment.

Existing inspections do not contain a supplier/sample-lot binding. Every attachment therefore requires an existing supporting record and a note explicitly attesting that the inspection represents this supplier's named sample lot. The server validates the inspection scope, but cannot independently prove that physical-lot claim. It freezes the actual inspection and all its recorded measurements, including their limits, plus the attestation and supporting-record metadata. It does not copy supporting document bytes. Later edits or deletion of the source inspection do not change this snapshot; attach a replacement before submission if the evidence needs correction.

Review and resubmit: the owner can submit only when every requirement has passing coverage. Submission freezes editing. Only the assigned independent reviewer may approve, reject or request more evidence. They must differ from the creator, owner and attached inspection authors. A request for more evidence retains the complete previous submission in history and clears all current coverage; the owner must reattach and attest every requirement before resubmission. Approval and rejection are terminal. The owner may cancel a nonterminal package with a reason, including during review. No transition changes production release, stock, supplier qualification or source inspection results.

Supplier-change traceability: optionally supply both a supplier change ID and requirement/action ID when creating the package. The server verifies that the existing, non-removed requirement's notice matches the supplier, item and revision. This is a traceability link only; it does not complete, approve or implement the supplier change.

The queue shows at most 200 packages, active first and earliest due first, with an explicit partial-list warning. Search, status/overdue filters and summary CSV cover the loaded list. Saved ?firstArticle=UUID links open packages omitted from that list. Inspect frozen evidence in each requirement or history entry, and export the full package and ordered event history as JSON. Inspection discovery shows the newest 100 final checks for the revision; an exact known check ID can be supplied even if omitted.

REST and AI-agent access

All six operations below are registered on the shared external Data API and generate MCP tools. Use the canonical OpenAPI contract and the connection's tools/list schemas, not guessed tool names. Reads require project data access, not cost access. Writes require a writable credential and workflow assignment. An API-key agent may own a package as its stable returned apikey:<key UUID> principal; it cannot become a member by changing the key's display name. The reviewer must be an independent workspace member. That member may act through delegated OAuth with live project data-write permission, a writable backing key and project:data:write consent. OAuth is used through MCP, not the REST Data API. The underlying member identity governs independence, while history preserves client attribution.

Method and relative pathOperation ID
GET /first-articlesgetFirstArticles
POST /first-articlespostFirstArticles
GET /first-articles/candidatesgetFirstArticleCandidates
GET /first-articles/{packageId}getFirstArticle
GET /first-articles/{packageId}/checksgetFirstArticleChecks
POST /first-articles/{packageId}/eventspostFirstArticleEvent

Candidate lookup accepts kind=supplier|item|evidence and optional q (up to 200 characters, 100 results). Creation needs a client-generated UUID id. Events need a new UUID id, the current package version, a nonblank note, and kind: attach, submit, request_changes, approve, reject or cancel. Only attach accepts and requires requirement (the exact key), checkId and supportId. The note is the lot attestation for attach and the rationale for other actions. Actor and principal come from authentication, never the request body.

After an uncertain response, retry the unchanged body with its original ID and version. The same authenticated principal and payload replay safely, including after a later state transition; conflicting reuse or a stale new command returns 409. Refresh and review before making a new decision. Package updates and append-only events commit atomically.

Supplier change control

Open Costing → Supplier change control → Explore supplier changes to record and review a supplier's proposed material, process, tooling or production-site change. This is your internal assessment of a supplier notice, not a supplier portal, supplier consent or regulatory certification. Approval and actual implementation are separate states. Nothing automatically changes supplier qualifications, prices, inspections, released records or stock.

Record the notice: choose a live price-enabled supplier/source record, add 1–25 distinct affected records, and link an existing notice document or supporting record. Describe the current state and proposed change, add an optional supplier reference, and set the notice received date, proposed effective date and review due date. The received date cannot be in the future; the proposed effective date cannot precede it. Dates use UTC. Retrospective notices are allowed and retain their recording timestamp. The proposal, dates, reviewer and affected scope remain fixed; cancel and replace an incorrect notice.

Each affected item captures its identity, name, reference and latest recorded revision when the notice is saved. An item without a recorded revision is explicitly unrevisioned. These are scope snapshots, not certification that a revision is released or effective. Supporting records preserve identity, name and type, not file bytes. Reviewers must open and evaluate the actual evidence.

Assess and prepare: assign a notice owner and an independent reviewer. The owner records an impact assessment explaining affected requirements, risks and needed inspections or requalification. Add up to 50 lifetime requirements, including removed ones, with a category, title, owner and due date. Describe scope and acceptance criteria in the requirement and reason. Only a requirement's assigned owner can complete it, with a supporting record and evidence note. Completion is an evidence-backed attestation, not an automatically checked inspection verdict or qualification status. If no requirements are needed, explain why in the assessment.

Reopen insufficient completion evidence or remove an inapplicable requirement with a reason; original entries remain in history. To change a requirement's assignment or definition, remove it and add its replacement before submission. The notice owner can be reassigned outside independent review, without changing requirement owners. The reviewer cannot create or own the notice, assess its impact, own a requirement or complete one.

Review: the assigned notice owner can submit only with an assessment and no pending requirements. Submission freezes assessment and requirements. The assigned independent reviewer may approve, reject or request more evidence. A request for more evidence returns the notice for work and clears the current assessment while preserving its history; the owner must reassess before resubmitting. Rejection is final. Approval fixes the reviewed scope but does not mark implementation complete.

Implement: after approval, the assigned notice owner records the actual implementation date, supporting evidence and note. The date must fall between approval day and today inclusive, in UTC; explain any difference from the proposed effective date. Implementation is final. Before implementation, the reviewer can withdraw an approval with a cancellation reason. Cancellation before approval is available to writers. Cancelled, rejected and implemented notices are terminal and cannot be reopened.

The queue prioritizes active notices and earliest due dates, loading at most 200. Open review work uses the review due date; approved work uses the proposed effective date. A date before today in UTC is overdue, but today is not. Pending requirements retain their own due dates in detail. Filters and summary CSV exports cover only the loaded list and explicitly flag partial results. A saved ?supplierChange=UUID link opens a notice even when it is omitted from the queue. Lists and history show 25 rows per page. History CSV includes all events; JSON includes the frozen definition, current state and complete history. CSV exports protect against spreadsheet formulas. Ordinary source deletion does not erase notice evidence; an explicit whole-project data reset removes it.

REST and AI-agent access

All five endpoints below are shared by the app and external REST API and are available as generated MCP tools from the canonical OpenAPI contract. Paths are relative to /x/{workspaceId}/api/v1/projects/{projectId}/data. Reading requires project data access, not cost access: these responses contain no prices. Writes require a read-write API key or a signed-in/delegated member with data-write access. OAuth MCP agents additionally need project:data:write; live role and grant restrictions still apply. The dashboard entry sits within Costing, whose navigation permissions remain unchanged.

Method and pathOperation IDPurpose
GET /supplier-changesgetSupplierChangesBounded queue, partial flag, capabilities and assignment principal.
GET /supplier-changes/candidatesgetSupplierChangeCandidatesSearch with required kind=supplier, item or evidence and optional q, at most 200 characters; up to 100 live records.
POST /supplier-changespostSupplierChangesCreate a frozen notice with a caller-generated UUID.
GET /supplier-changes/{changeId}getSupplierChangeCurrent state and complete ordered history in one database snapshot.
POST /supplier-changes/{changeId}/eventspostSupplierChangeEventAssessment, requirements, review decisions and implementation.

List and detail responses return actor, the authenticated assignment principal, and canWrite. An API-key agent may assign itself as notice or requirement owner using its returned apikey:UUID principal. It cannot impersonate a member, nominate another key or use a key name as identity. The reviewer must be an independent workspace member, who may act directly or through an authorized delegated MCP agent. Delegated agents use that member's identity for assignments and independence; switching clients does not allow self-approval. History retains both the authenticated principal and the agent/client audit actor. Callers cannot supply either identity in a write body.

Each event requires a new command id, current version, kind and nonblank note. Supported kinds are assess, add_action, complete_action, reopen_action, remove_action, reassign, submit, request_changes, approve, reject, implement and cancel. The OpenAPI request schema and operation description specify each kind's fields. Omit unrelated fields. Requirement completion and implementation require evidenceId; implementation also requires occurredOn.

For an uncertain response, retry the unchanged command with the same ID and original version. The recorded principal and body must match. A replay returns 200 with replayed:true; a new write returns 201. A stale version or conflicting ID reuse returns 409: refresh and review before preparing a new command. REST's optional Idempotency-Key provides its usual transport-level retry protection too. Read-only credentials cannot mutate, and a discoverable tool never overrides assignment, independent-review or lifecycle restrictions.

Supplier qualifications and approval reviews

In Costing → Supplier qualifications, record a review of an existing price-enabled source record. Project administrators with cost access can record decisions; users with cost-read access can view them. This is an internal dashboard workflow, not an external data API or supplier-portal operation.

Choose a supplier-wide, part-type or specific-part scope, then record Pending review, Approved, Conditional approval or Suspended. Approval and conditional approval require an expiry date, valid through that day in UTC. A review always needs a rationale; use it to state conditions and safeguards. The reviewer and recording time are captured automatically.

Link up to ten existing project records, including supporting documents. Review history preserves the evidence record IDs, names and references captured at review time. It does not copy attachments, freeze linked record contents or pin a document revision. Use document revision history when tracing the underlying file versions.

The most-specific applicable review wins: part, then part type, then supplier-wide. An expired or pending part review does not fall back to a broader approval. Any applicable suspension overrides approval, including a narrower approval, and stays in force until a new review at that scope replaces it. Corrections and renewals append a new review rather than editing history; conflicting saves must be reloaded and reviewed again.

Use the review queue to find expired approvals or approvals expiring within 30 days, search by supplier, scope, reviewer or rationale, inspect review history, and export the current filtered view as CSV.

RFQ quote comparisons show supplier qualification for the requested part. The award dialog refreshes it, and the server checks again inside the award transaction. Anything other than a current unconditional approval requires a separate qualification override reason, in addition to the commercial award rationale. This includes unreviewed, pending, conditional, expired and suspended suppliers. The assessment, selected review and exception reason are saved with the award and retained in quote CSV exports, even if a later review changes the supplier's status. Older awards explicitly have no qualification snapshot.

Boundary: this is a sourcing review and exception workflow, not a universal purchasing block. Direct approved-source edits and change-request workflows are not blocked by this feature. RFQ quantity, quote validity, exclusion and existing change-control checks still apply; a qualification override does not bypass them.

Purchase orders

Open Costing → Purchasing → Purchase orders. Search order or supplier names and filter by order status, transmission status, supplier, workspace-member owner and inclusive expected-date bounds. The register pages 50 orders at a time; changing a filter returns to page one. Search is limited to 200 characters. Unsupported bookmarked order/transmission statuses fall back to all.

Filters remain available while the first read loads, the register is empty or a read fails. Refresh retains all current filters and the page, moving to the last available page if results shrink. Clear filters resets search, both statuses, supplier, owner and date bounds on page one. Previous rows and counts stay hidden until current results arrive. Obsolete reads are cancelled; late responses cannot replace current results. Failed server reads show generic access guidance. Long order and supplier names wrap, with totals and status below them on phones.

Open an order to inspect its current terms, fulfillment, linked deliveries and event history. If your current access allows reading but not changes, detail explains that the order is read only and offers Refresh to recheck permissions. Download current version and Download last dispatched version use the currently authorized order read. Retrying, refreshing or paging history hides previous details and download actions until the new response. Leaving detail or changing account/project cancels its outstanding read and prevents a late response from restoring old details. Opening detail pauses the background register read; closing it refreshes the list. Transmission retries remain a separate action on the recorded dispatch.

Purchase order recovery: in the authenticated app, creation, draft edits, amendments, issuing, sending amendments, acknowledgement, closure and cancellation save the complete request in this browser before sending. The account, project and order scope keeps unresolved requests separate. Closing a tab retains them; reopening never sends automatically. Existing tab requests migrate only after durable storage succeeds. Open Purchase order recovery, review the saved inputs and choose Retry saved order change. Retry preserves the original command ID, version, line quantities/prices, dates, terms and lifecycle evidence. A matching command ID and boolean replay confirmation retire it and open the current order.

Saved terms and evidence stay hidden until a fresh project/order read confirms current access. Explicit retry uses current server authorization and generic recovery errors. An uncertain original stays recoverable through permission denial, changed owner validation or a generic conflict, without dismissal. A confirmed stale-order refusal allows review and dismissal after a fresh order read at dismissal confirms access. Retrying removes dismissal first; a lost result stays recoverable. Different commands cannot replace an unresolved request for the same order. Supported browser locks refuse simultaneous retries without queuing a send. Other orders remain independent.

Recovery is local to the same browser; clearing or eviction of browser data can remove it. Storage failure prevents a new order request from being sent. A first definite refusal lets you correct the draft; a stale edit/lifecycle refusal requires refreshing current state while preserving your authored inputs. Retry with the same signed-in account or API credential that originally submitted the request. Committed creation and edit requests can replay after an assigned owner leaves or your account/key display name changes, while preserving the original owner choice and attribution. Keep the saved request unchanged; substituting a new owner or changing other submitted terms conflicts with its original identity. New requests still validate the current owner. Current project read/write and cost access remain required; revoked access and read-only credentials prevent replay. Supplier email transmission status and its retry action remain separate.

API clients must keep the original command ID, submitted terms and credential identity when a response is uncertain. New order commands are bound to the stable account ID, API key ID, or delegated grant and account ID. A new credential with the same visible name or email cannot replay another identity’s request. Delegated clients need both data-read and data-write scopes, a writable credential and current project permissions. Read-only callers can inspect authorized orders; the register and detail report that changes are unavailable. Older requests retain their existing stored-hash matching rules, including recorded owner normalization, and are not automatically assigned a new caller identity. Purchase-order creation, edit and lifecycle MCP tools provide typed command-ID and replay confirmations.

Supplier delivery commitments, dispatch and receiving

Open Purchasing → Deliveries. Record a commitment with an existing price-enabled supplier/source, an item, optional order reference, promised quantity, explicit unit and promised date. Native purchase orders can supply eligible issued or acknowledged lines; the server checks their remaining allocation. Use one commitment per agreed delivery line. Recording a commitment does not place an order or authorize payment. Reading requires current project data-read and cost access. Recording also requires data-write access, a writable credential and stable caller identity; delegated callers need both data scopes and credential project restrictions apply. Register/detail canWrite reflects those current permissions.

Amend the promised quantity or date with an explanation, retaining the original commitment and amendment history. Quantity cannot fall below active dispatch totals, and native purchase-order allocation still applies. Cancel only while there are no active receipts or dispatches. Cancellation retains its reason and history. Valid physical receipts must not be voided just to change a promise. Recording delivery evidence does not change inventory, accounting, qualification or PO status.

Delivery recovery: in the authenticated app, ordinary creation, awarded RFQ handoff, amendments and cancellations save the complete UUID and request body in this browser before sending. They survive closing all tabs and are scoped to the account, project and delivery. RFQ handoffs also retain the original source RFQ. Existing tab requests migrate only after a durable save. Reopening never sends automatically. Review the saved quantity, promise, original version, owner and notes as applicable, then choose Retry saved delivery change. Saved terms appear only after a fresh authorized delivery read, or source RFQ read for a handoff; unavailable access keeps them hidden. Explicit retry checks current server write access even while terms are hidden. Matching UUID and boolean replay confirmation clear only that exact request and open the current delivery. Atomic storage and supported browser locks prevent another tab replacing or simultaneously resolving it. Browser clearing or eviction removes local recovery; other devices do not share it.

A generic conflict, access denial or validation error after uncertainty preserves the exact request without dismissal. HTTP 409 delivery_stale proves that no matching receipt exists and the current state or native order allocation refused the change. That refusal permits review and dismissal only after a fresh readable check. Retrying first removes the dismissal option. New creation, amendment and cancellation receipts bind to the account ID, API key ID or delegated grant plus account ID; visible caller renames preserve replay and original attribution. A different identity cannot replay a new protected receipt. Creation replay checks the original submitted request before current owner defaulting, membership or allocation, so an owner leaving does not block an already committed request. New commands still need a current member owner. Legacy receipts retain their existing hashes and recorded owner normalization without identity backfill. Typed ordinary creation/event MCP outputs include the command UUID and boolean replay confirmation; private receipt metadata stays hidden.

Awarded RFQ handoff recovery: the complete handoff survives closing all tabs in this browser under the original account and project. Reloading never submits automatically. Saved inputs stay hidden until a fresh source RFQ read confirms current access, and retry errors use generic copy. Explicit retry requires matching command UUID and boolean replay confirmation before clearing the request or opening the delivery; incomplete confirmation and generic 400/404/409/422 responses preserve the exact original without dismissal. Only recorded delivery_stale proof permits dismissal after a fresh RFQ read. Legacy rejected flags without proof stay recoverable, and migration cannot overwrite a newer durable attempt. A new delivery UUID cannot bypass an unresolved handoff for the same RFQ, including in another tab. New receipts bind the source RFQ and original submitted terms to the stable caller identity. Exact replay precedes current owner membership, RFQ award derivation and native order allocation, preserving the original supplier, item, title, quantity, owner and attribution even after later RFQ changes or caller renames. Legacy handoffs use their existing hash and frozen creation fields without identity backfill. New handoffs still require a current award and member owner. A proven refusal requires refreshing and reviewing the current RFQ and eligible order links before a fresh command; authored delivery terms remain available. The handoff remains dashboard-only and has no external REST or MCP tool.

Changing account or project stops recovery and prevents a late result from opening a record in the new context. For all purchasing recovery, if storage cleanup fails after a confirmed response, retain the exact request for confirmation; the recovery message does not claim that nothing was sent.

Dispatch: record a positive quantity in the delivery’s unit, actual dispatch date, optional expected arrival date, carrier and tracking reference. Dispatch dates cannot be in the future; an arrival estimate cannot precede dispatch. Split dispatches accumulate up to the current promised quantity. Amend the commitment first when more quantity is agreed. Correct an active dispatch by appending its full replacement with an explanation, or void an erroneous entry with an explanation; the original remains visible. Arrival estimates are separate from the promised date. Carrier and tracking details are manually recorded text.

The workbench and linked sample request show active dispatched quantity, undispatched quantity and estimated quantity in transit. Estimated transit is max(active dispatched − gross physical receipts, 0). Explicit shipment balances use linked receipts; unassigned arrivals do not confirm a particular shipment arrived. The displayed arrival estimate is the latest date among active shipments with remaining receiving capacity; the carrier/reference belongs to the most recent active entry. A dispatch does not record receiving, inspection acceptance, a sample receipt, a review or quality completion. Automatic carrier feeds and notifications are not included.

GET/supplier-deliveries/{deliveryId}/dispatchesRead + cost

Typed MCP tool get_supplier_deliveries_by_deliveryid_dispatches returns current delivery, newest-first dispatches, total, limit, offset, hasMore and canWrite in one repeatable-read snapshot. Accepts limit (default 50, clamped to 200), nonnegative offset, activeOnly=true|false and optional receiving=awaiting|overdue|received. A receiving filter always selects active shipments, including when activeOnly=false. Filtering precedes counts and paging. Awaiting means a positive linked receipt balance; received means its whole dispatched quantity has linked physical receipts. Overdue additionally requires an estimate before today in UTC; today and missing estimates are not overdue. These views use physical arrival, independently of inspection acceptance. Entries retain rootId through corrections; active entries include receiving received/remaining quantities, status (in_transit, partially_received or received), overdue and optional lastReceivedOn. Superseded entries omit current receiving progress. Active flags account for the entire history, including replacements on another page. Requires current project data read and cost access; delegated callers also need data-read scope. Unknown or foreign-project delivery IDs return 404. Private, no-store.

POST/supplier-deliveries/{deliveryId}/dispatchesWrite + cost

Typed MCP tool post_supplier_deliveries_by_deliveryid_dispatches requires a new command UUID id, current delivery version and kind: dispatch, correction or void. Dispatch/correction requires quantity and dispatchedOn, with optional expectedOn, carrier (120 characters), tracking (200) and notes (4,000). Correction/void also requires active targetId and explanatory notes. Void omits quantity, dates, carrier and tracking. Requires project data read/write, cost access, a writable credential and stable authenticated principal; delegated callers need both read/write scopes. Dispatch/correction history is limited to 1,000 entries; voids remain available. Private, no-store; unsupported queries/properties are refused.

Dispatches serialize with receipts, amendments and cancellation using the delivery’s version. New commands return 201. Retry an uncertain outcome with the unchanged UUID, normalized body, delivery and authenticated principal: exact replay returns 200 even after later changes or account/agent renames. Changed ID reuse returns 409. A new stale head, inactive target or capacity refusal returns 422 without recording anything; refresh and review before creating a fresh command. Original attribution remains captured in history, while private replay metadata is not returned.

The browser durably stores the exact dispatch command before sending it, scoped to the account, project and delivery. Retry saved dispatch command explicitly retries it after a lost response or closed tab; no automatic submission occurs. Access-denied and uncertain results retain the command, while definite validation rejection restores authored inputs for correction. Storage failure blocks a new save. Browser clearing or eviction loses local state; cross-device recovery is not included.

Receipts: record positive decimal quantities with up to 12 integer digits and 6 fractional digits, using the commitment’s unit. Received dates cannot be in the future. Split receipts accumulate; outstanding quantity bottoms out at zero and over-delivery is reported separately. Receipt quantities do not imply inspection acceptance, and recording a receipt never changes an inspection verdict.

Receive a shipment: choose Receive this shipment on an active dispatch, or select a shipment in the receipt dialog. The suggested quantity is the shipment’s remaining quantity; reduce it for a partial arrival. One receipt assigns its entire quantity to one shipment; record separate receipts for multiple shipments. The server checks the current delivery version, active shipment, remaining quantity and a receipt date on or after dispatch. Existing and unassigned receipts are never matched automatically. Past-estimate warnings mean quantity still lacks a linked receipt; check unassigned arrivals before following up.

Receipt API: POST /supplier-deliveries/{deliveryId}/events accepts optional dispatchId on receipts/corrections. Corrections inherit the original frozen binding unless dispatchId moves the receipt or unlinkDispatch: true explicitly removes it; those options cannot be combined. Linked receipt creation, correction, unlinking and voiding require project data read/write, cost access, a writable credential and stable principal, with both scopes for delegated callers. New commands return 201; identical body/UUID/principal replay returns 200 with original attribution after later changes. Changed reuse returns 409; capacity, inactive shipment or receipt-before-dispatch refusal returns 422 without committing. Shipment corrections keep links and cannot reduce quantity below linked receipts or move dispatch after receipt. A shipment with effective linked receipts cannot be voided. Voiding an erroneous physical receipt releases capacity; returns and rejected inspections do not undo physical arrival.

Receipt history and CSV capture shipment identity, dispatch date, carrier and tracking at receiving. Explicitly using that receipt as sample-review evidence freezes the same trace while current tracking can change. Arrival does not perform review or close a quality action. Receipt, correction and void retries preserve the exact command UUID and body and require a matching UUID plus boolean replay confirmation. New authenticated receiving events, including unassigned receipts, bind to the stable account ID, API key ID or delegated grant plus account ID. Visible caller renames preserve replay and original attribution; another identity cannot replay a protected receipt. Legacy empty-principal events retain their existing matching rules without backfill.

Receiving recovery: in the authenticated app, commands are saved in this browser before sending, scoped to the account, project and delivery. Closing a tab retains them; reopening never sends automatically. Commands already saved in the current tab migrate before that copy is removed. Review the saved request and choose Retry saved receiving change; success reloads the delivery and its current projections. Recovery is local to this browser; clearing or eviction of browser data can remove it. Supplier returns, purchase orders and their lifecycle changes also have durable browser recovery; ordinary delivery creation, amendments and cancellations now also have durable browser recovery. Awarded RFQ handoff and other purchasing requests retain their existing tab recovery.

Current read access is checked before showing saved quantities, receipt/shipment/evidence identities or notes. If access cannot be confirmed, inputs stay hidden and an explicit retry still checks current write permission on the server. Retry errors use generic copy to protect inputs if access changes during the save.

An uncertain original stays recoverable through generic 400, 404 or 422 responses, access denial and ambiguous version/principal conflicts, without dismissal. On the receiving event endpoint, HTTP 409 receipt_stale, receipt_limit or return_changed, and HTTP 422 dispatch_changed, prove that no matching receipt was found before the command was refused. Only these recorded refusals permit dismissal, after a fresh delivery read at dismissal confirms access. An older rejection flag without a recorded refusal keeps the request recoverable. Retrying removes dismissal before sending. Different commands cannot replace an unresolved command for the same delivery, including across tabs; supported browser locks refuse simultaneous retries or dismissal without queuing a send.

Receipt corrections: a correction appends a replacement and supersedes the selected receipt atomically, preserving the original. A void removes an erroneous receipt from effective totals without deleting it; neither represents a physical return. Both need a reason. Concurrent stale writes are rejected for review, and an unchanged uncertain command reuses its identifier. Each commitment permits up to 1,000 receipts/corrections; voids remain available afterwards.

Attach an existing supporting document or record and optionally select an inspection directly on the item or naming it in its captured subject reference. The picker shows the latest 100 live matching checks. Evidence names, check identity and inspector verdict are snapshotted; underlying document bytes are not frozen. Open the linked check to see current evidence. Receipt links do not automatically add checks to the scorecard’s separate supplier-attributed quality cohort or assign supplier fault.

Shipment exceptions: choose Awaiting shipment receipts, Overdue shipments or Unassigned receipts in the delivery status filter. These views include accepted-complete deliveries and exclude cancelled commitments. An unassigned physical arrival may complete a delivery and reduce its estimated transit to zero while its shipment still awaits an explicit receipt link. Verify the original receipt and correct it to select the shipment; record a new receipt only for another physical arrival. Opening an awaiting or overdue result selects the matching shipment view. The shipment selector also offers full history, all active shipments and fully received shipments. Changing a view resets paging and hides prior results while loading.

Lists are paged. Filter by inclusive promised-date bounds, search supplier/item/order names and choose current status, including In transit. GET /supplier-deliveries accepts status=awaiting_shipment_receipts|overdue_shipments|unassigned_receipts alongside the existing status values. Counts and filters precede paging in one read snapshot. Open history to inspect actors, timestamps, superseded entries and correction reasons. Export every matching delivery-summary page or receipt event history as formula-safe CSV with exact quantities. Summary CSV includes linked physical received quantity, unassigned physical received quantity, shipment receipt balance and overdue shipment count separately from acceptance and delivery completion. Unlike units are not aggregated. The scorecard links back to delivery history. Source sample purge removes its request association while procurement commitments, dispatches and receipts remain independent.

The date fields filter promised dates, including both boundary dates. Search, status and Clear filters stay available during loading and failed reads. Refresh keeps the current page, applied dates, search and status, moving to the last available page if results shrink. Clear filters restores all statuses on page one while keeping applied dates. Applying or resetting dates restarts paging and keeps search/status. Previous rows and counts stay hidden until current results arrive; obsolete reads are cancelled. Failed server reads use generic access guidance. Scroll the table horizontally to see hidden columns, or use arrow keys while the table is focused. Export matching deliveries uses the filters selected when it starts. Leaving the register, changing account/project or applying/resetting dates cancels further export reads and suppresses the old download. Receipts update linked purchase-order fulfillment; closing a fulfilled order is a separate action. Starting a supplier return reopens a closed native order.

Rework execution and reinspection

Open Nonconformances, select an NCR with a recorded rework disposition, then choose Start rework job. The NCR must still be dispositioned and must have a positive whole affected quantity and an explicit unit recorded before its material-review decision. One non-cancelled job covers that entire quantity. Item, revision, lot, quantity, unit and NCR decision are preserved as a snapshot; this does not create a production order or release material.

Record clear work instructions, an existing workspace member as operator, and a due date. The instructions and due date remain fixed. The job uses the NCR's assigned verifier, who cannot create or operate it. Reassign the operator with a reason when ownership changes; previous assignments and work remain in history. Only the assigned operator can record partial completion. Each entry needs a positive whole quantity, actual completion date and execution evidence. Dates cannot precede job creation or be in the future, and completion cannot exceed the affected quantity.

Reinspection: after the full quantity is completed, record a new check in the item's Measurements workflow and link it to the job. The candidate picker shows the latest 100 eligible checks: the same entity, captured revision (including no revision), and inspection kind; a later round than the original failure; created after the current cycle's effective completion entries; and checked no earlier than completion and no later than today in UTC. A check on another revision, a prior approval, or a different item cannot satisfy this step. The reviewer must still assess whether the inspection adequately covers the affected lot; a sample result does not automatically prove full-quantity acceptance.

The linked check and its unsized characteristic measurements, captured limits, units and methods are frozen in the rework evidence. Before verification the server checks that the linked approved inspection still exists and that this recorded evidence has not changed. If it was deleted or edited, record and link a fresh later check. Linked documents remain references, not frozen file bytes. Per-entity size-chart actuals are not part of this check snapshot.

Failure and corrections: a rejected reinspection requires another full-quantity cycle under the same instructions. Starting another cycle resets current completion to zero but preserves earlier work and failed inspections. It is also available after full completion when the work needs repeating. Void a completion entry only to correct an input mistake, only in its current cycle and before linking an inspection; then enter the correct work. An empty first-cycle job can be cancelled and replaced. Jobs with work from prior cycles cannot be cancelled. Never void real work merely to cancel a job.

Independent verification: the assigned signed-in verifier reviews execution evidence and the fresh approved inspection. That person must be independent of the creator, operators across every cycle, and reinspection author. Verification is permanent. It does not automatically verify or close the NCR: those remain separate governed actions. Once any structured rework job has been started, NCR verification is blocked until a job is independently verified; cancelling a job does not bypass that gate. Cancel an unfinished empty job before cancelling its NCR. NCRs that never started structured rework retain their existing note-based verification workflow.

Use Nonconformances → Rework queue → Explore rework to find jobs by NCR, item, operator, verifier or status. Active jobs are prioritized, earliest due first; dates before today in UTC count as overdue until verified or cancelled. Up to 200 jobs are loaded, with partial warnings applying to search, filters and CSV exports. Open an omitted job's source NCR to see its jobs. History is paged on screen; CSV exports include all loaded events and their sequence, cycles, actors, dates, corrections and inspection metadata. The JSON evidence export includes the job definition, complete event history and frozen check/measurement snapshots.

Reading requires project data-read access. Creating or recording events requires data-write access and a signed-in member; these new routes are internal to the app, not public API or MCP tools. Version checks reject stale changes, and unchanged retries reuse command IDs to avoid duplicate completion. History survives ordinary source-record deletion and is removed during an explicit whole-project data reset. There are no automatic inventory, accounting, production-scheduling or release changes.

Scrap execution and disposal evidence

Open Nonconformances, select an NCR with a recorded scrap disposition, then choose Start scrap job. The NCR must still be dispositioned, with a positive whole affected quantity and an explicit unit recorded before its material-review decision. One non-cancelled job covers that entire quantity. The item, revision, lot, quantity, unit and NCR decision are preserved as a snapshot.

Record disposal instructions, an existing workspace member as operator, and a due date. Instructions and the due date remain fixed. The job inherits the NCR verifier, who cannot create or operate it. Reassignment requires a reason and retains prior assignments in history. Only the assigned operator can record actual disposal, including partial quantities up to the remaining quantity.

Each disposal entry requires a positive whole quantity, actual disposal date, method, evidence note and an existing supporting document or record. Destination or recipient and certificate or reference are optional. Dates use UTC and must fall between the NCR scrap-decision date and today, inclusive; retrospective recording after that decision is allowed. Supporting record identity, name and type are snapshotted, not the underlying document bytes. Verification requires a human review of the actual supporting evidence; a link alone does not prove physical destruction.

Corrections: void an incorrect disposal entry with a reason, then record the correct entry. Voiding preserves the original record and reduces the recorded total; it is only for recording mistakes and never reverses physical disposal. The assigned verifier cannot void entries. A job can be cancelled only when it has no effective disposal quantity. Its history remains available and a replacement job can be started.

Independent verification: after the full quantity is disposed, the assigned NCR verifier reviews the quantities, methods, dates and supporting evidence. The verifier cannot be the creator, an operator or a participant in disposal corrections. Verification permanently locks the job. Verify and close the NCR separately after reviewing other obligations. Once structured scrap has started, the NCR cannot be verified until a scrap job is verified; cancelling every job does not bypass this requirement. Cancel an unfinished empty job before cancelling the NCR. NCRs that never started structured scrap retain note-based verification.

Use Nonconformances → Scrap execution queue → Explore scrap queue to search by NCR, item, lot, operator or verifier and filter by status or overdue work. Active jobs appear first, earliest due first. A due date before today in UTC remains overdue until verification or cancellation, even when the full quantity is disposed. Up to 200 jobs are loaded; partial warnings apply to filters and summary CSV exports. Open an omitted job's source NCR to see its jobs. Lists and history show 25 rows per page. History CSV and JSON exports include the entire loaded event history, including voided originals, sequence, actors, dates and evidence metadata; summary and history CSVs protect against spreadsheet formulas.

Reading requires project data-read access. Changes require data-write access and a signed-in member. These routes are internal to the app, not public API or MCP tools. Version checks reject stale changes; retrying an unchanged request reuses its command ID to avoid duplicate disposal. History survives ordinary source-record deletion and is removed by an explicit whole-project data reset. This workflow does not move stock, post accounting entries, schedule disposal services, certify regulatory compliance or automatically close an NCR.

Supplier returns and replacement tracking

In Costing → Purchasing → Deliveries, open a delivery and choose Start supplier return on an effective receipt. Alternatively, open an NCR with a recorded return to supplier disposition and follow its return link to select the delivery and receipt. The NCR is preselected and must concern that receipt’s item. The optional NCR picker shows the latest 100 eligible cases, including dispositioned, verified and closed cases; cancelled or unrelated cases cannot be linked.

Reserve: enter a positive return quantity and reason. The supplier, item and unit come from the original delivery; the receipt identity and NCR decision snapshot remain traceable. Quantities support up to 12 integer digits and 6 decimal places. All non-cancelled returns against the receipt reserve their full quantity, including closed returns, so concurrent cases cannot return the same goods twice. A reserved receipt cannot be corrected or voided. Cancelling a case with no effective movements releases its reservation; cancellation preserves its history.

Authorize and execute: record the supplier’s authorization reference, agreed replacement due date and supporting note. These terms are fixed once recorded. Then record actual return shipments and replacement receipts, including partial quantities, dates, shipment references and optional supporting documents or records. Shipment totals cannot exceed the reserved quantity, and replacements cannot exceed effective shipments as of the replacement date. Dates cannot precede the original receipt or be in the future. Quantities always use the original unit; receiving a replacement does not imply inspection acceptance.

Correct and resolve: void an erroneous shipment or replacement entry with a reason, then record the correct entry. This preserves the original rather than editing it. Voiding is for entry mistakes, never real stock movements; dependent replacement entries must be corrected first if removing a shipment would make the timeline inconsistent. Close a case only after its entire reserved quantity has been shipped and replaced. Closure is permanent. Cancellation is allowed only with no effective shipments or replacements. This workflow covers replacement resolution, not refunds, credits, write-offs or replacement-before-return arrangements.

Open Costing → Purchasing → Returns to see unresolved cases, remaining shipment quantities, shipped quantities awaiting replacement and overdue replacement promises. A case is overdue when its fixed replacement due date is before today in UTC and the full reserved quantity has not been replaced; today is not overdue. Search and status filters run on the server before counts and pagination. The app pages through the register, opens complete history and exports all matching pages as formula-safe summary CSV; history CSV includes superseded movements. Return creation loads every return for the delivery before calculating receipt availability.

Search and status controls remain available while results load or a read fails. Changing filters, pages or refreshing hides previous rows and counts until the current request completes; superseded reads are cancelled. Choose All to find closed cases even when there are no unresolved returns. Clear filters restores unresolved cases on the first page. Refresh keeps the current filters and page, moving to the last available page if results shrink. On narrow screens, scroll the table horizontally to read all columns; arrow keys also scroll the focused table. Export matching returns uses the filters selected when it starts; leaving the register or switching account or project cancels further export reads and the download.

Every event retains its actor, recording time, command identity and sequence. Once a case reaches 1,000 events, each new shipment or replacement entry must record its full remaining quantity; voids and terminal actions remain available so the case can still be resolved. Linked record names are snapshots, not frozen document bytes. Evidence survives ordinary source-record deletion and is removed by an explicit whole-project data reset.

Supplier return recovery: creation, authorization, shipment, replacement, void, cancellation and closure save their exact request in this browser before sending, scoped to the signed-in account, project and return. Closing a tab retains an uncertain request. Reopening never sends automatically; choose Retry saved return change to retry its original UUID, body and version. Existing tab return commands migrate only after the durable save commits. Current server read access is checked before showing saved receipt/delivery identities, quantities, owners, reasons, references or notes. Denied reads and server retry errors use generic copy; retry still checks current server write authorization. Success opens the return with refreshed projections.

Unknown responses, including generic 400, 404 and 422, access denial and command/principal conflicts retain recovery without dismissal. HTTP 409 return_changed or return_history_limit proves no matching receipt was found before refusal; dismissal then requires a fresh return read at dismissal. Older rejection flags without a recorded refusal stay recoverable. Retrying removes dismissal first. Different commands cannot replace an unresolved return request; supported browser locks refuse simultaneous retries without queuing a save. A first definite entry refusal retains the authored draft for refresh and review with a fresh UUID. Storage failure before sending leaves that draft editable. Recovery stays in the same browser; clearing or eviction of browser data can remove it.

Reading requires current project data-read and cost access. Changes also require data-write access, a writable credential and stable signed-in account, API key or delegated grant/account identity. Delegated callers need the relevant read/write scopes. Read-only keys can read but cannot create or record return events. canWrite reflects these restrictions. All responses are private, no-store; request bodies and query parameters are checked strictly.

GET/supplier-returns

REST and typed MCP get_supplier_returns return returns, total, limit, offset, hasMore, generatedAt and canWrite. Optional deliveryId, q (200 characters), status, limit (default 50, clamped to 200) and nonnegative offset filter and page the register. Omitted status selects all cases; the app defaults to unresolved. GET /supplier-returns/{returnId} and MCP get_supplier_returns_by_returnid return the complete event history. Private replay hashes and principals are never returned.

POST/supplier-returns

MCP post_supplier_returns shares the REST contract: id, deliveryId, receiptId, positive quantity, reason (4000 characters), owner (current eligible workspace member, 320 characters) and optional ncrId. An empty owner defaults to the caller. New commands return 201 with id and replayed:false. Exact UUID/body/principal replay returns 200 and replayed:true before revalidating owner membership or receipt availability. Owner departure, caller renames and later return events preserve exact replay and original attribution. Legacy records retain their original body hash and frozen owner normalization, with no principal backfill.

POST/supplier-returns/{returnId}/events

MCP post_supplier_returns_by_returnid_events accepts id, current version, kind (authorize, ship, replace, void, cancel or close) and required note, plus terms appropriate to that action: targetId for void, quantity and occurredOn for movements, reference and dueOn for authorization, and optional evidenceId. New writes return 201; exact replay returns 200 before current head/lifecycle checks. New events bind stable caller identity; legacy events keep body-only replay. Changed body or principal with an already-used UUID returns 409 return_command_conflict: preserve an uncertain request and retry its exact terms with the original identity. 409 return_changed (stale head, receipt/capacity or lifecycle refusal) and 409 return_history_limit prove no commit; refresh and review before submitting a fresh UUID/current version.

The original physical delivery receipts remain intact. Non-cancelled returns reduce accepted fulfillment, and effective replacements restore it; replacement dates participate in accepted on-time performance against the current delivery promise. Opening a return on a closed native purchase order reopens that order with an attributed event so its accepted balance can be resolved. Return entry does not post inventory or accounting movements or automatically complete supplier qualification, inspection, NCR or CAPA work. Linking a case does not independently establish supplier fault.

Supplier performance scorecards

In Costing → Supplier performance scorecards → Explore scorecards, choose a price-enabled supplier/source record and optionally apply a date range. The scorecard requires project cost-read access and is an internal app workflow, not a new public integration endpoint. It reports evidence for that exact record, not a consolidated company score across every material or source associated with the same manufacturer.

Date cohorts: inclusive UTC bounds select RFQs and CAPAs by creation date, checks by checked-on date, and deliveries by promised date. Their outcomes are current, not reconstructed at the selected end date. A later response or award still appears for an RFQ created in the period. The loaded timestamp makes the observation time explicit; refresh to update it.

Sourcing: one opportunity per RFQ and source, even after replacement links. Response rate is rounds with a portal submission divided by rounds with an issued link. Link creation is not proof of delivery; the separate email count means a successful invitation send was recorded, not recipient delivery or reading. Median response time runs from the first link issued to the first portal submission, including replacement delays, and excludes invalid timing. Quotes without a portal submission are shown separately, so manual entry cannot inflate portal response rates. Award share is wins divided by quoted rounds currently awarded or closed: open rounds are excluded, and closed rounds without an award are included. Current excluded quotes remain visible.

Quality: only checks directly on the source record or explicitly identifying it in the captured subject reference are included. The current approved-source relationship and name matches never assign historical responsibility. Inspector decisions stay grouped by check kind and are not measurement defect rates or proof of supplier fault. Open a check to inspect its notes, measurements and available evidence. Deleted checks and deleted subject records are excluded.

CAPA: cases qualify through an attributed live check or a direct source-record snapshot. A case linked to multiple checks is counted once. Direct snapshots survive check deletion; reference-based attribution requires the live check. Draft, open and pending-verification cases count as active. Overdue uses the loaded UTC date; active-case action totals exclude closed and cancelled cases. The evidence table retains all included cases with their current uncompleted actions.

Delivery performance: on-time-in-full is the number of commitments fully received by their fixed promised date divided by non-cancelled commitments promised before the loaded UTC day. Commitments due today or later are not yet judged. Mean quantity fulfillment is the unweighted average received/committed ratio across that same cohort, capped at 100% per commitment. Late receipts improve fulfillment but not on-time performance; unlike units are never pooled. Cancelled commitments are excluded and counted separately. Commitments recorded after their promised date are included and explicitly counted as retrospective entries. Corrections can change metrics, which describe recorded evidence rather than independently verified delivery claims.

Each section offers source drill-down and formula-safe CSV export with record identifiers, cohort bounds, observation time and attribution fields. Up to the latest 2,000 records per section are loaded; warnings and CSV flags identify partial sections, whose metrics describe only that subset. Narrow the date range to reduce it. Missing evidence is explicitly insufficient data, never a perfect score. These are mutable current records, not an immutable historical scorecard. No overall rating, currency pooling, automatic qualification change or inferred supplier blame is produced.

Private calibration certificates

In Calibration → Record calibration, attach a PDF, PNG or JPEG, or reuse a certificate already recorded in the same project. Files must contain 1 byte to 10 MiB. The upload commits with the calibration event: a failed save leaves no abandoned certificate. History offers authenticated downloads, and its CSV export includes certificate ID, filename, content type, size and SHA-256.

Certificates are private evidence, separate from the ordinary file library's public CDN URLs. Upload and reuse require project data-write permission and the Files & Images entitlement. Newly stored bytes count toward workspace storage; reuse does not count them twice. Existing evidence remains downloadable with project data-read permission after a downgrade. Certificates cannot be replaced through the API or deleted while referenced by an event.

POST/measurement-equipment/{equipmentId}/calibrationsWrite

For upload, send multipart form data containing exactly one metadata JSON value and one file. For reuse, send the normal JSON body with certificateAssetId. Upload and reuse are mutually exclusive; attachment-free JSON requests still work.

record calibration with a private certificatebash
curl --fail-with-body -X POST "$BASE/measurement-equipment/$EQUIPMENT/calibrations" \
  -H "X-API-Key: $KEY" \
  -F 'metadata={"calibratedOn":"2026-09-01","nextDueOn":"2027-09-01","result":"passed","certificateReference":"CERT-2026-01"}' \
  -F '[email protected];type=application/pdf'
GET/calibration-certificates?offset=0Read

Returns { certificates, hasMore }, with at most 50 metadata records per page. Increase offset by the number received while hasMore is true. Each certificate has id, name, contentType, sizeBytes and sha256; private bytes are never embedded in list/history JSON.

GET/calibration-certificates/{certificateId}/downloadRead

Downloads the original file as an attachment with Cache-Control: private, no-store. Read-only API keys may download within their project scope; no public download URL is created.

Quality audit packs

For retained packages and optional Algorand publication, open the project’s Evidence page. The evidence workflow also supports historical release certificates and configuration releases.

Open the project’s Quality workbench and use its Inspections, Issues, Risk & controls, or Calibration tabs. Choose Export audit pack, set the inclusive UTC date range, select quality areas, and download the ZIP. You can cancel preparation or retry after a failure without losing the selection. The pack contains linked CSVs, private calibration certificate files, a README explaining scope and joins, and a versioned checksum manifest. From an entity, its Quality tab shows related totals and opens the same workbench already scoped to that entity.

AreaDate used to select recordsIncluded evidence
InspectionsCheck date (checked_on)Checks, subject identity, measured values and recorded tolerances, referenced plans, and matching plan/entity/revision assignments, waivers and events.
CalibrationCalibration date (calibrated_on)Events, equipment, impact reviews, containment resolutions and attached private certificates.
CAPACase opening date (created_at)Cases, source snapshots, actions and retained event history.
NonconformancesCase opening date (created_at)Cases and retained disposition/lifecycle history.

All files come from one consistent database snapshot. Mutable records show their current state at export, not their historical state at the range end. Selected cases and calibrations carry full retained child histories, including events outside the date range. To include a case opened earlier but worked during the interval, widen the opening-date range.

GET/quality-audit-packRead

Provide from, to and areas exactly once. Dates use YYYY-MM-DD; the end must be on or after the start. Areas are a comma-separated unique selection of inspections, calibration, capa and nonconformances. Project data-read permission is required; read-only keys are supported. Exporting existing certificates does not require the Files & Images entitlement.

download a scoped evidence packbash
curl --fail --show-error --get "$BASE/quality-audit-pack" \
  -H "X-API-Key: $KEY" \
  --data-urlencode 'from=2026-09-01' \
  --data-urlencode 'to=2026-09-30' \
  --data-urlencode 'areas=inspections,calibration,capa,nonconformances' \
  --output quality-audit-2026-09.zip

manifest.json records the project, selection, generation time, CSV row counts, and each other file's byte length and SHA-256, including README.txt but not the manifest itself. Join CSVs using their IDs. An event's certificate_asset_id joins files[].certificate.id in the manifest; certificate filenames use UUIDs, while metadata preserves their original names. Reused certificates appear only once. CSVs use UTF-8 and escape potential spreadsheet formulas with an apostrophe. Checksums detect changes; they are not signatures or proof of authenticity.

Scope matters: linked IDs may refer outside the selected areas or dates. General image/file evidence, deleted checks, requirements without a selected check, entity field values, revision bodies, release certificates, FMEA, control plans and project audit logs are excluded. This is an evidence pack, not a complete backup or importable project bundle.

The limit is 10,000 rows per CSV and 100 MiB of uncompressed evidence, with 60 seconds for preparation. Oversized exports return 422 export_too_large; preparation timeouts return 504 export_timeout. Narrow the range or select fewer areas to retry. Preparation failures do not publish partial ZIPs, and exports are never silently truncated. Responses are private, no-store; keep downloaded packs private because they contain evidence and actor details.

Evidence packages and wallet publishing

Open Evidence in the project sidebar. If publishing is not configured, the page guides administrators through setup before showing the package workspace. Existing packages remain accessible through View existing packages. To create private packages first, expand Use Evidence without publishing and choose Continue without publishing. Creation preserves a private package; publication is a separate action.

Capture a package

Choose New package, select a source, then Continue to package → Create package. Quality packages use an inclusive UTC date range and selected quality areas. Release packages capture a chosen historical certified revision and its required document files. Configuration packages capture a chosen immutable configuration release, its full-depth BOM and pinned certificate evidence. Later record changes leave captured package bytes intact. A quality date range selects roots from the capture snapshot; it does not reconstruct their state at the range end.

Set your organization’s publisher

  1. Use an Algorand account controlled by your organization. We recommend Pera Wallet; its help center explains wallet setup. Fund the account on the network used by your ManyRows deployment.
  2. On the Evidence page, a workspace owner chooses I have a wallet, pastes the account’s 58-character public address and chooses Save publisher address. The package workspace opens after setup. Owners can also manage the address in Workspace settings → Organization publisher or use Publisher setup after initial setup. ManyRows validates the address and records changes in the workspace security audit.
  3. A project administrator chooses Sign and publish with Pera, connects the package’s publisher wallet and approves the prepared transaction. The organization’s account pays the network fee; ManyRows shows the configured fee ceiling. Ordinary unrekeyed accounts are supported; multisig accounts, rekeyed accounts and unattended signing are not supported.

A publication typically costs 0.001 ALGO, roughly US$0.0001 at an illustrative rate of US$0.12 per ALGO. Exchange rates and network fees can vary; Pera shows the final fee before approval. See Algorand’s fee documentation and ALGO’s USD price.

Public address only

Your organization is the publisher. ManyRows never requests or stores wallet private keys or recovery phrases. Viewers and independent verifiers do not need a wallet. The deployment selects the network; a customer does not choose it in the Evidence UI. If publication is unavailable because the blockchain connection is missing, ask your deployment administrator to configure it. Package creation, downloads and local verification remain available.

Only a salted fingerprint is recorded on Algorand through a zero-ALGO self-payment. Package contents remain private in ManyRows; the publisher address and publication timing are public. Obtain a publisher’s identity independently before relying on its receipt.

Download and verify

Download the ZIP and its Content integrity receipt for local verification. After confirmation, download the Download verification receipt. Open Verify a package without signing in. The ZIP and receipt stay in your browser. Local verification checks file integrity; it does not establish publisher identity or blockchain inclusion. Optional online verification uses independently trusted publisher information and ledger endpoints for the receipt’s network. It sends public ledger requests, not your package files.

A confirmed commitment links the recorded evidence to a blockchain transaction. It does not prove physical inspection, current certification or the truth of the original records.

Changes, retries and removal

Packages already assigned to a publisher keep that address. A package created before wallet setup receives its publisher, and any missing network identity, when its first transaction is prepared. Changing or clearing the workspace address leaves existing receipts and issued transactions intact. Issued transactions can finish with their original publisher; an unissued package assigned to an old address requires restoring that address or removing the package and explicitly creating another.

Retries reuse the original transaction. Expired attempts require review through Check ledger now or Close expired attempt; a new package is never published automatically. Packages without an issued transaction, confirmed packages and safely closed attempts can be removed to reclaim storage. Once a wallet transaction is issued, removal waits for confirmation or verified closure after expiry. Public commitments and previously downloaded copies cannot be recalled. Storage and publisher daily limits appear on the Evidence page.

Read through REST or MCP

Authorized integrations can read package metadata, download archives and receipts, and inspect lifecycle activity. Publisher configuration, package creation, signing, ledger checks, closure and removal require the appropriate signed-in human permissions; API keys and delegated agents cannot perform those actions.

GET/quality-audit-packages?scope=projectRead

List retained packages of all three kinds, including packages whose source records were removed. Requires complete project field visibility. Omit scope to list quality packages only. Lists return at most 100 packages. Package statuses are private, awaiting_signature, submitted, confirmed, needs_attention or closed.

GET/release-evidence-packages?revisionId={uuid}Read

List packages for a historical certified revision. Configuration packages use /configuration-evidence-packages?configurationReleaseId={uuid}. Both lists require complete project field visibility.

For each kind, use its package resource (quality-audit-packages, release-evidence-packages or configuration-evidence-packages): GET /{resource}/{packageId} returns metadata; append /download for the ZIP, /integrity-receipt for local integrity verification, /receipt for a confirmed blockchain receipt, or /events for activity. GET /evidence-events provides project activity, including removed packages, in pages of up to 100 events and requires complete project field visibility. Pass the returned nextBefore cursor as before to load an older page.

MCP exposes the read tools generated from these routes, including get_quality_audit_packages, get_release_evidence_packages and get_configuration_evidence_packages. Publication writes are marked human_only and omitted from the executable tool catalog. Use the dashboard’s canonical API contract for the current input and response schemas.

Operational attention

GET/project-home/attentionRead

Read one compact, permission-aware snapshot of the project's operational work queues instead of polling each register independently.

The response can include overdue milestones and nonconformances, open CAPA actions, high-risk FMEAs, control-plan gaps, overdue supplier changes, late deliveries, open RFQs, and configuration-rollout attention. Commercial counts and configuration-rollout details are omitted when the caller lacks the corresponding permission; omission is different from a zero count.

Each section is calculated independently. If one calculation fails, the available counts are still returned with partial: true and its stable name in failedSections. Treat a named failure as unknown rather than zero, and retry the snapshot before acting on an apparently clear queue. Responses are private, no-store.

Specs, calendars and quality

The PLM toolkit is reachable over the API, not only the UI. Each of these is configured: false rather than an error when the record’s type does not declare that plane, so a client can ask without knowing the schema first.

Measurements

GET/entities/{id}/size-specRead

The graded measurement chart: points of measure across the size range, with tolerances, plus explicit sizeCount and rowCount summaries. Its ETag identifies the specification definition (not recorded actuals); poll with If-None-Match or protect an ingestion with If-Match.

POST/entities/{id}/size-spec/actuals/ingestWrite

Atomically preview or apply up to 200 sample measurements against the current base-size targets and tolerances. POM names must be unique and match the chart; send null to clear an actual. The result gives every submitted POM's target, deviation, pass/fail/unknown status, previous value, and set/cleared/unchanged action, plus missingPomNames for chart rows omitted from a partial run.

SPEC_ETAG=$(curl -si "$BASE/entities/$ID/size-spec" -H "X-API-Key: $KEY" | awk -F': ' 'tolower($1)=="etag" { print $2 }' | tr -d '
')
curl -sS -X POST "$BASE/entities/$ID/size-spec/actuals/ingest" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -H "If-Match: $SPEC_ETAG" -H "Idempotency-Key: sample-run-001" \
  -d '{"preview":true,"measurements":[{"pomName":"Chest","value":52.4},{"pomName":"Waist","value":41.0}]}'

Preview first, inspect summary and measurements, then send the same measurements with preview:false and a fresh Idempotency-Key because the body changed. The write is all-or-nothing. A stale chart returns 412 precondition_failed; successful runs keep the definition ETag stable. Governed live records remain measurable, while frozen records and change-request working copies are refused.

GET/entities/{id}/size-curveRead

Per-size order quantities, resolved from the referenced product’s current ordered size range. The response carries an ETag for conditional polling and safe replacement.

PUT/entities/{id}/size-curveWrite

Atomically replace the complete size split with up to 200 quantities. Keys must come from GET’s sizes; omitted sizes become zero and {} clears the curve. The response reports previous/current quantities, deltas, actions, and total changes.

curl -sS -X PUT "$BASE/entities/$ORDER_ID/size-curve" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -H "If-Match: $CURVE_ETAG" \
  -d '{"preview":true,"quantities":{"S":90,"M":180,"L":90}}'

Preview first and inspect would_increase, would_decrease, and would_clear; preview rolls the curve, total-field writeback, and audit entry back while returning the unchanged baseline ETag. Apply with preview:false. A configured integer total is updated atomically on ungoverned orders. Governed live orders accept the operational curve but return totalSynced:false, leaving the definition field for a change request. Frozen records and working-copy clones are refused. If the key cannot read a field that defines the curve, both routes return 404.

GET/entities/{id}/characteristicsRead

The record’s unsized measurable specification — the same idea as a size spec for something that is not graded. Its ETag identifies the ordered names, nominals, tolerances, units, and methods; use If-None-Match when polling or If-Match when ingesting results.

Time & Action

GET/entities/{id}/ta-scheduleRead

The record’s Time & Action schedule: milestones counted back from the anchor date, with derived status and an explicit milestoneCount. The response carries an ETag; use If-None-Match for polling and If-Match before reconciliation.

POST/entities/{id}/ta-milestonesWrite

Add a milestone to a configured schedule, up to 200 milestones total. Names are limited to 200 characters, and an owner must be a current workspace member. PATCH or DELETE /entities/{id}/ta-milestones/{mid} to update its definition/progress or remove it.

PUT/entities/{id}/ta-milestones/orderWrite

Replace milestone display order with a complete permutation of up to 200 IDs: { "milestoneIds": [...] }.

PUT/entities/{id}/ta-milestones/syncWrite

Atomically make up to 200 milestones the complete desired schedule, including order. Existing rows match by id, or by an exact unique name when no id is supplied. The result reports added, updated, removed, moved, and unchanged rows and returns the fully resolved schedule. Send [] to clear it.

curl -sS -X PUT "$BASE/entities/$ID/ta-milestones/sync" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -H "If-Match: $SCHEDULE_ETAG" \
  -d '{"preview":true,"milestones":[
    {"id":"'$LAB_ID'","name":"Lab dip approved","offsetDays":-21},
    {"name":"PP sample","offsetDays":7,"predecessorName":"Lab dip approved"}
  ]}'

Names must be unique, no longer than 200 characters, and every predecessor must be present in the submitted schedule. Preview executes the real write and target-resolution path, then rolls it back; inspect would_add, would_update, would_remove, and would_move, then apply against the unchanged baseline ETag. A concurrent schedule or anchor edit returns 412 precondition_failed. Writes require a configured schedule; archived, frozen, and working-copy records are refused, while governed live records remain schedulable.

Quality

GET/entities/{id}/checksRead

Check records against this record — rounds, verdicts and dispositions.

GET/checksRead

Every check in the project, filterable. Use entityId for the exact checks attached to one entity.

GET/check-kindsRead

The project’s check vocabulary as { kinds, total, isDefault }. Reuse its ETag with If-None-Match to receive 304 while it is unchanged.

POST/entities/{id}/checksWrite

Record a check round with a discovered kind and an approved, conditional, or rejected verdict. To satisfy a release inspection, also send the current requirement’s inspectionPlanId and revisionId; the server rejects stale or mismatched bindings. DELETE /entities/{id}/checks/{checkId} to remove a round entered in error.

GET/inspection-plansRead

List the project’s revision-bound quality gates for release-controlled entity types, including inactive plans. Plan configuration remains a human admin-app decision.

GET/inspection-requirementsRead

Derive current inspection work by active plan, live entity, and latest approved revision. Filter with status=pending|failed|passed|waived, mine=true, or an exact entityId. Passing evidence and waivers never carry across revisions.

PUT/entities/{id}/inspection-plans/{planId}/assignmentWrite

Override the plan’s default inspector and due date for the record’s exact current revision. The inspector must be a workspace member.

GET/inspection-plans/{planId}/eventsRead

Read the append-only plan, assignment, waiver, and revocation trail. Reasoned waiver issuance and revocation remain human admin-app decisions.

GET/measurement-equipmentRead

List the instrument register with current, overdue, failed, uncalibrated, out-of-service, or retired calibration status. Use mine=true for equipment owned by the caller.

GET/measurement-equipment/{equipmentId}/calibrationsRead

Read immutable passing and failed calibration events, including their effective date range, certificate reference, optional private certificate metadata, notes, and actor. See calibration certificates for uploads, reuse and authenticated downloads.

POST/measurement-equipment/{equipmentId}/calibrationsWrite

Append { calibratedOn, nextDueOn, result, certificateReference, notes }. A failed or expired latest result immediately makes the instrument ineligible for new governed inspections. When an inspection plan requires equipment, send calibrationEquipmentId with the check; the server snapshots the exact calibration event valid on checkedOn.

GET/measurement-equipment/{equipmentId}/calibrations/{eventId}/impactRead

For a failed calibration, trace governed checks that relied on the preceding passing calibration and may require review or reinspection. Once assessed, the response includes its immutable review. A reinspection_required disposition adds derived pending/completed status and replacement-check evidence for every affected check.

POST/measurement-equipment/{equipmentId}/calibrations/{eventId}/impact/reviewWrite

Record the failed event's one durable assessment with required notes and a disposition of no_impact, reinspection_required, or containment_required. A duplicate returns 409; calibration and inspection evidence remain unchanged.

POST/measurement-equipment/{equipmentId}/calibrations/{eventId}/impact/containment-resolutionWrite

Close a containment_required assessment once with required evidence notes. The actor and timestamp are retained, duplicate closure returns 409, and the resolution appears on subsequent impact reads.

POST/entities/{id}/checks/ingestWrite

Create a round, its ordered measurements, and an optional rejected-round disposition atomically. Any invalid characteristic, disposition, assignment, or date rolls the complete command back.

Read GET /entities/{id}/characteristics first; it returns { characteristics, total }. Send its ETag as If-Match, so a tolerance change cannot race the laboratory result. Set preview:true to execute every check without persistence; the preview omits the provisional check id. On apply, require 201 and applied:true. The response includes the complete resolved check and a summary of inTolerance, outOfTolerance, and observational measurements. Tolerance never derives the verdict: an out-of-tolerance measurement can still carry an explicit concession decision.

ingest one complete lab resultbash
curl -X POST "$BASE/entities/$PART/checks/ingest" \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -H "If-Match: $SPEC_ETAG" -H "Idempotency-Key: $COMMAND_ID" \
  -d '{ "kind": "fabric_test", "verdict": "rejected",
    "measurements": [
      { "characteristic": "Tensile strength", "value": 47.2 },
      { "characteristic": "pH", "value": 7.1 }
    ],
    "disposition": { "disposition": "rework", "note": "Retest after treatment" }
  }'
PUT/entities/{id}/checks/{checkId}/measurementsWrite

Replace the round’s measured characteristic values without deriving or changing its verdict. New writes capture the specification definition fingerprint and measurement method alongside the original limits.

Open a record’s Measurements tab and choose Explore trends to explore unsized characteristic measurements recorded on check rounds. This does not chart graded size-chart actuals. Apply inclusive check-date bounds or an exact characteristic-name filter, then select a series. Each series keeps characteristic, check kind, unit, record revision, referenced subject, inspection plan, captured specification definition, method and limits separate. Identical specification definitions share a fingerprint; no unit conversion or cross-version pooling is performed.

The chart shows values against their captured tolerance band, with count, average, minimum, maximum, range, first-to-last change and out-of-tolerance rate. Observational measurements have no pass/fail result and are excluded from that rate. The inspector’s verdict remains a separate decision. These summaries are not process-capability statistics or automatic drift alarms.

Select a chart point with a click or keyboard, or open its check from the values table, to inspect the current check, notes and available evidence. Export downloads the selected series with full-precision values, captured limits and provenance. The view loads at most the latest 5,000 matching measurements; a warning and CSV flag identify partial results, including excluded invalid numeric samples. Narrow the date or name filter to reduce the query.

Older measurements retain their captured limits but are labelled legacy because their specification fingerprint and method were not recorded. Those facts cannot be reconstructed from the current specification. This view reflects currently stored checks, not an immutable audit log: replacements change the history and deleted checks disappear. It requires project data-read access; the trend-view routes are internal to the app, not new public integration endpoints.

PUT/entities/{id}/checks/{checkId}/dispositionWrite

Set the corrective decision for a rejected check (use_as_is, rework, or scrap), including owner/due/closure dates. DELETE the same path to clear a disposition entered in error.

POST/ncrsWrite

Promote one rejected inspection into a stable NCR-0001-style case. The NCR snapshots the check, evidence identifier, affected record, and current revision; one check can open only one NCR. Include containment, affected lot/quantity, owner, and due date in the create or open-case PATCH.

GET/ncrsRead

Read the filtered NCR queue. Pass concessions=true to return non-cancelled cases with a use_as_is disposition, including closed cases. This matches the concessions count from GET /ncrs/dashboard and is separate from the controlled /concessions register. The dashboard also returns active, overdue, review, and disposition counts; GET /ncrs/{ncrId} returns the complete case and append-only history.

POST/ncrs/{ncrId}/submit-reviewWrite

Freeze an open, contained case for a named independent MRB reviewer. That signed-in reviewer records use_as_is, rework, scrap, or return_to_supplier through POST /ncrs/{ncrId}/disposition; use-as-is is an explicit concession approval. A separately assigned signed-in verifier must call POST /ncrs/{ncrId}/verify with execution evidence before the case can close. Active NCRs on the current revision block release certification with open_nonconformance.

PUT/ncrs/{ncrId}/capaWrite

Link an active NCR to an existing project CAPA when the material failure needs systemic corrective action. DELETE the same path to remove an editable link; both operations remain visible in NCR history.

POST/fmeasWrite

Create a dfmea or pfmea against a record's latest immutable revision. Add failure modes through POST /fmeas/{fmeaId}/items; severity, occurrence, and detection are each 1–10 and risk priority is their server-derived product.

GET/fmeasRead

List revision-bound analyses with an identities directory for the returned registers’ account names. Pass attention=high-risk to find non-cancelled registers with an open failure mode whose current risk priority meets that register’s high-risk threshold. Pass attention=overdue-actions to find draft or in-review analyses with an open action due before today. The dashboard counts overdue actions, so its count may exceed the number of matching analyses. Status, kind, entity, text, and signed-in personal-work filters can narrow the list further.

GET/fmeas/{fmeaId}Read

Read the register, paginated failure modes, and history. The identities object gives current display names and usernames for referenced account emails; historical or external actors may be absent. Pass itemsAttention=high-risk to return open modes at or above the register’s risk threshold on non-cancelled analyses, or itemsAttention=overdue-actions to return overdue open actions in draft or in-review analyses. itemsTotal and item pagination then describe that filtered set. The register and event history remain complete.

POST/fmeas/{fmeaId}/submit-reviewWrite

Freeze a complete analysis for an independent reviewer. High risks must be mitigated with action evidence and residual scores or explicitly accepted with rationale by that signed-in reviewer before approval. Pending FMEAs block current-revision certification with fmea_pending.

PUT/fmeas/{fmeaId}/items/{itemId}/linksWrite

Connect a realized failure mode to same-project NCR and CAPA evidence. The link change is retained in the FMEA's append-only history.

POST/control-plansWrite

Create a revision-bound production plan from an approved current pfmea. Every failure mode whose initial RPN met the PFMEA threshold is promoted into a draft control line. One non-cancelled plan may govern a PFMEA; cancelled attempts remain as history and can be replaced.

GET/control-plansRead

List revision-bound plans with their high-risk coverage, incomplete-line counts, and an identities directory for account names. Use attention=incomplete for active plans with incomplete lines, attention=uncovered for active plans with uncovered high-risk PFMEA items, or attention=gaps for either condition. Status, entity, text, and signed-in personal-work filters can narrow the list further.

GET/control-plans/{controlPlanId}Read

Read the register, paginated control lines, and history. The identities directory resolves account names referenced by the register, returned lines, and events; historical or external actors may be absent.

PATCH/control-plans/{controlPlanId}/lines/{lineId}Write

Complete a draft line with its characteristic, specification and measurement method, sample size and frequency, control method, reaction plan, owner, check kind, and an active inspection plan for the same entity type and kind. Specification and PFMEA source facts are snapshotted as evidence.

POST/control-plans/{controlPlanId}/submit-reviewWrite

Freeze a fully covered, executable plan for a named independent reviewer. That signed-in reviewer can request changes or approve with a mandatory note. A high-risk approved PFMEA blocks certification with control_plan_missing or control_plan_pending until its plan is approved.

GET/control-plans/{controlPlanId}Read

Read the plan, ordered lines, coverage counters, revision snapshot, approval evidence, and newest-first immutable history. If a bound inspection plan is later disabled or mismatched, release readiness returns control_plan_drift.

POST/control-plans/{controlPlanId}/reopenAdmin

A signed-in project admin records a required reason and returns an approved current-revision plan to draft so drifted inspection bindings or other production controls can be corrected. Prior approval remains in immutable history, and independent review is required again before the plan clears release readiness.

POST/capasWrite

Create a draft, risk-ranked corrective and preventive action case, optionally linked to an initial rejected or conditional check. Each case receives a stable CAPA-0001-style project reference. For an uncertain create response, resend the identical body with the same optional UUID requestId: the existing case returns with 201. Reusing that ID with different input returns 409.

GET/capas/{capaId}Read

Read the complete investigation: linked failure snapshots, containment and root cause, risk priority, corrective/preventive actions, independent approval, effectiveness outcome, and append-only history. GET /capas provides the filtered worklist and accepts entityId to match cases with a source from that entity; GET /capas/dashboard provides project-wide risk and recurrence counters.

PATCH/capas/{capaId}/actions/{actionId}Write

Send only { "complete": true } to finish an action, or { "complete": false } to reopen it, without changing its kind, title, owner, or due date. To edit those fields, read the action from GET /capas/{capaId} and send the full action metadata with its updatedAt. A missing version returns 428; a stale version returns 412, so read the action again before editing.

POST/capas/{capaId}/submit-verificationWrite

Freeze a completed action plan for a named workspace-member approver. The case must have a root cause, effectiveness criteria and date, and at least one completed action. The named human records effective to close or ineffective to reopen through POST /capas/{capaId}/verify; an API key cannot impersonate that sign-off, and an ineffective result requires revised action work before resubmission.

Requirements

GET/entities/{id}/requirementsRead

Material requirements for this order — the BOM exploded against its quantities.

For an order-free explosion of a single unit, use parts summary instead.

rowCount reports the number of returned material requirement rows.

supplierCount reports distinct readable suppliers across requirement rows.

unsourcedRowCount reports requirement rows that still need a readable supplier assignment.

supplierWithheld distinguishes supplier redaction from genuinely unassigned requirement rows.

unitWithheld distinguishes redacted units from genuinely unitless requirements.

wastageWithheld is true when access policy caused returned requirement quantities to be safely de-inflated.

Reuse the response ETag as If-None-Match to receive 304 while requirements are unchanged.

The typed response includes state, orderQuantity, zeroQuantity, effectivityTracked, and rows with material identity/images, qtyPerGarment, totalQty, unit, and supplier identity/images.

Counts and batch reads

GET/entities/by-idsRead

Batch-fetch records by id, across any type, up to 200 per call.

POST/entities/by-referencesRead

Batch-fetch up to 200 records by stable type/reference pairs in one query, preserving input order and missing-item results.

GET/entities/countsRead

Record and collection-member totals, collection volume by owning type in collectionMembersByTypeKey, a separate totalTrashed with trashedByTypeKey, lifecycle totals in byState, and the same lifecycle split per stable type key in byTypeState. Per-type totals are available in portable byTypeKey and compatible UUID-keyed byType maps. Pass a nonblank type=part (max 100 characters) to scope every total to one stable type key; explicit blank scopes are rejected. Use includeEmpty=true to include configured types and explicit zero-valued active, archived, and draft buckets. The response echoes both applied controls. Reuse its ETag with If-None-Match to receive 304 while the representation is unchanged.

GET/entities/{id}/reference-countRead

How many distinct live records reference this one — check before you delete. Reuse its ETag with If-None-Match to receive 304 while the count is unchanged.

GET/entities/{id}/alternate-usagesRead

Reverse approved-alternates: the BOM lines that list this record as an approved alternate.

GET/entities/{id}/activation-closureRead

Preview the draft closure that activating this record would sweep in. Every member includes its stable typeKey, display type name, isRoot, and write-ready version/etag; top-level total, requiresCRCount, and governed summarize the activation path. Reuse the response ETag with If-None-Match to receive 304 while the resolved closure is unchanged.

Receiving events

For low-latency push, have a project POST to you when something changes. Endpoints are set up in the admin app under Project → Webhooks. Use the durable change feed when you need pull-based recovery or do not operate a public receiver.

There is no API for managing endpoints. Deliberately: an API key that could register a forwarding endpoint would be an escalation path out of every other limit placed on that key. Registering, pausing and rotating are admin actions.

Each endpoint gets a signing secret, shown once when it is created and replaceable by rotating. Store it as you would any credential — it is the only thing that distinguishes our POST from anyone else’s.

what arriveshttp
POST https://your-receiver.example.com/hook
Content-Type: application/json
X-ManyRows-Event: entity.change_approved
X-ManyRows-Delivery: 0199f0e0-...     # stable across retries — dedupe on this
X-ManyRows-Timestamp: 1785628800      # unix seconds
X-ManyRows-Signature: 9f86d081...     # lowercase hex, see below
bodyjson
{
  "event": "entity.change_approved",
  "operation": "approve",
  "entityId": "0199...",
  "entityTypeId": "0199...",
  "source": "admin",
  "changes": [
    { "fieldKey": "status", "label": "Status", "type": "select",
      "old": "In development", "new": "Approved" }
  ]
}

changes carries each changed field’s before and after. Ids are not resolved to names — fetch the record when you need more than the diff.

Event list

eventstext
entity.created            entity.updated            entity.trashed
entity.restored           entity.archived           entity.unarchived
entity.purged             entity.activated          entity.change_approved
change_request.submitted  change_request.rejected   change_request.changes_requested

entity.change_approved is the one most integrations want: a governed change went live and a revision was welded for it.

Subscribing to nothing in particular means all events, including event types added later — so an endpoint registered today does not go quietly out of date.

Verifying signatures

Compute HMAC-SHA256(secret, "<timestamp>.<raw body>") and compare it in constant time against X-ManyRows-Signature. Use the raw body bytes, before any JSON parse or re-serialisation — re-encoding changes the bytes and the signature will not match.

verify.pypython
import hmac, hashlib

def verify(secret, headers, raw_body):
    ts  = headers["X-ManyRows-Timestamp"]
    sig = headers["X-ManyRows-Signature"]
    mine = hmac.new(secret.encode(),
                    f"{ts}.".encode() + raw_body,
                    hashlib.sha256).hexdigest()
    return hmac.compare_digest(mine, sig)

The timestamp is inside the signed material, so a captured request cannot be replayed later. Reject anything whose timestamp is more than a few minutes old.

Delivery semantics

  • At least once. A crash between our POST and our recording of your response redelivers. X-ManyRows-Delivery is stable across retries — dedupe on it.
  • Any 2xx is success. Everything else is retried, including 4xx: we cannot tell a permanent misconfiguration from a deploy briefly returning 404.
  • Six attempts, backing off 30s → 2m → 8m → 32m → 2h, then given up on. A given-up delivery is kept and shown in the admin app rather than deleted — "we stopped trying to tell you" is the most useful thing that history can say.
  • Respond fast. We wait 10 seconds. Acknowledge, then process asynchronously.
  • Ordering is not guaranteed. Retries reorder events by design. If order matters, sort on your side or re-fetch the record.
  • Pausing an endpoint stops delivery of its queued backlog too, and resumes it on re-activation. Events raised while it is paused are not backfilled.

Events are written in the same transaction as the change they describe, so nothing is announced that then rolls back.

Assets

Upload bytes, then bind the returned descriptor through a normal entity create / update on an image or file field.

POST/assets/imagesWrite

Multipart file; ≤ 20 MiB; jpeg / png / webp / gif / avif.

POST/assets/filesWrite

Multipart file; ≤ 10 MiB.

GET/assets/images/by-hash/{sha256}Read

Dedup lookup.

GET/assets/files/by-hash/{sha256}Read

CAD documents

Upload native models, drawings, and exchange files through POST /assets/files, then attach the descriptor through a File field. The Documents tab uses those same fields. In entity type settings, restrict a File field to the extensions you need; normal record permissions and review rules still apply when binding the file.

Supported CAD extensions (case-insensitive): step, stp, iges, igs, stl, 3mf, obj, x_t, x_b, sat, sab, dxf, dwg, sldprt, sldasm, slddrw, ipt, iam, idw, ipn, f3d, f3z, catpart, catproduct, catdrawing, prt, asm, drw, par, psm, dft, jt. CAD bytes are stored unchanged as application/octet-stream with attachment disposition. The descriptor retains the uploaded filename, size, and SHA-256; the storage key uses a lowercase extension.

The limit remains 10 MiB per file. Uploading does not validate the CAD format, generate a viewer, extract properties or BOMs, or synchronize a CAD application. Dependencies are not collected automatically. For assemblies with linked files, or numbered filenames such as part.prt.1, upload a ZIP preserving the original filenames and relative paths (also limited to 10 MiB). ZIP contents are not extracted.

upload, then bindbash
# 1. upload the bytes (multipart) -> returns a descriptor for the stored image
curl -X POST "$BASE/assets/images" -H "X-API-Key: $KEY" \
  -F "[email protected]"

# 2. bind the descriptor on an image field via a normal create/update
curl -X PATCH "$BASE/entities/$ID" -H "X-API-Key: $KEY" \
  -d '{ "attributes": { "photo": <descriptor from step 1> } }'

Oversized / unsupported uploads return 413 too_large / 415 unsupported_media_type.

Client libraries

There are no official client libraries for the Data API yet. It is plain JSON over HTTP behind an X-API-Key header, so any HTTP client will do. Use the hosted MCP server or generate a typed client from the canonical OpenAPI 3.0 specification; every example on this page runs as written.

Tell us which language you use and what you need to integrate. Business includes product enhancement requests; implementation scope and availability are discussed with the team. Ask us.

Not yet available

Schema import is intentionally additive. Use governed schema migrations for supported renames, metadata, constraints, options, configuration, and deprecation. Deletion, stable-key changes, field-type/reference-target rewrites, and workflow policy remain admin-dashboard operations.

Cost and pricing are admin-app only.

Permanent Trash operations (bulk-purge, empty-trash, delete-all, wipe) and release/configuration decisions are admin-app only.