This is the full developer documentation for DUST Platform (DICE) # DUST Platform Docs > Connect physical objects to trusted digital records. Track, verify, share, and ship them — in the app or through the API. [DICE web app`dice4.dustid.io`First-party UI](https://dice4.dustid.io)[DUST API`apid.dustid.io`Core platform](https://apid.dustid.io)[DUST Account portal`authd.dustid.io`Accounts & API keys](https://authd.dustid.io) ## The platform at a glance [Section titled “The platform at a glance”](#the-platform-at-a-glance) New to DICE? [Core concepts](/use/core-concepts/) explains how all of these pieces fit together, with diagrams — start there. Threads Digital records for physical items: names, typed fields, files, identifiers, and a full event history. Identifiers & scanning Bind DUST, QR, barcode, data matrix, or NFC identifiers to threads. Identify unknown items and verify authenticity by scanning. Teams & sharing Organizations and teams scope everything. Share threads and folders across teams, and connect with partner organizations. Shipments & provenance Transfer threads and assemblies to other organizations, slice new threads from existing ones, and trace it all in the Fabric lineage explorer. ## Pick your track [Section titled “Pick your track”](#pick-your-track) [Use DICE](/use/getting-started/)End-user and admin guides for the DICE web app: getting started, scanning, folders, shipments, disclosures, FAQ. [Build with the API](/api/quickstart/)Quickstart, authentication, request conventions, and concept-first guides to the REST API. [Integrate & extend](/integrate/dust-go/)DUST Go, the dust-go-connect bridge, the React scanner, and skills for AI coding agents. [Reference](/reference/api/)The live API reference, environments, and the platform glossary. # Authentication and API keys > Create a Service Account, exchange its credentials for a bearer token, and keep credentials server-side. The DUST API authenticates every `/api/v1/*` request with a bearer JWT issued by AuthD, the DUST account service. API integrations act as a **Service Account** — a machine identity owned by your organization — never as a person. The flow is: 1. An organization admin creates a **Service Account** and issues it a credential (once). 2. Your integration **exchanges** the credential for a short-lived **bearer token**. 3. Send `Authorization: Bearer ` on API calls, and re-exchange when the token expires. ## Service Accounts [Section titled “Service Accounts”](#service-accounts) A Service Account is a first-class machine identity: it belongs to exactly one organization, it can be granted team access like a member, and every action it performs is recorded in the audit ledger as the Service Account — not as whichever employee happened to configure it. Its credentials can be rotated or revoked at any time without touching anyone’s personal account. Two credential types are available, and one Service Account can hold both: * **API key** — the simplest integration: exchange the key for a token with one HTTP call. * **OAuth2 client (`client_credentials`)** — for enterprise middleware (SAP Integration Suite, MuleSoft, Boomi, …) with built-in OAuth2 support. Note Personal API keys do not exist: keys belong to Service Accounts only. If you have an old user-owned key, it no longer authenticates — ask your organization admin for a Service Account key. ## Create an API key [Section titled “Create an API key”](#create-an-api-key) Service Accounts and their credentials are managed by organization admins in the AuthD portal at [authd.dustid.io](https://authd.dustid.io). 1. Sign in at [authd.dustid.io](https://authd.dustid.io) as an organization admin. 2. Open your organization page and select the **Service accounts** tab. 3. Create a Service Account (for example, “SAP Connector” or “Line 3 scanner station”). 4. Open **Manage** on the Service Account and create an API key. 5. Store the key in a secrets manager — treat it like a password. It is shown once. Caution If the Service accounts tab reports that service accounts are not enabled for your organization, contact [](mailto:support@dustidentity.com). ## Exchange the key for a bearer token [Section titled “Exchange the key for a bearer token”](#exchange-the-key-for-a-bearer-token) `GET /api/auth/token` takes the API key in the `x-api-key` header and returns a JWT. (APID proxies this to AuthD, so one base URL covers everything.) * curl ```bash curl -fsS "https://apid.dustid.io/api/auth/token" \ -H "x-api-key: $DUST_API_KEY" ``` * TypeScript ```ts const response = await fetch("https://apid.dustid.io/api/auth/token", { headers: { "x-api-key": process.env.DUST_API_KEY! }, }); const { token, expiresIn } = await response.json(); ``` Response: ```json { "token": "eyJhbGciOi...", "expiresIn": 900, "expiresAt": "2026-07-14T22:40:00.000Z" } ``` `expiresIn` is the token’s remaining lifetime in seconds; `expiresAt` is the same moment as an ISO 8601 timestamp. Use either to schedule the next exchange — don’t hardcode a lifetime. ## OAuth2 client\_credentials [Section titled “OAuth2 client\_credentials”](#oauth2-client_credentials) For platforms that speak OAuth2 natively, create an **OAuth client** on the Service Account instead of (or alongside) an API key. The client id and secret are shown once at creation. Request a token from the Service Account token endpoint with the standard `client_credentials` grant — `client_secret_post` (form fields) and `client_secret_basic` (HTTP Basic) are both accepted: ```bash curl -fsS "https://authd.dustid.io/api/auth/dust/service-accounts/token" \ -d grant_type=client_credentials \ -d client_id="$DUST_CLIENT_ID" \ -d client_secret="$DUST_CLIENT_SECRET" ``` Response (standard OAuth2 token response): ```json { "access_token": "eyJhbGciOi...", "token_type": "Bearer", "expires_in": 900 } ``` The resulting token is identical in shape and rights to one from the API-key exchange — use it the same way. If your middleware asks for a “token URL”, use the endpoint above. ## Use the bearer token [Section titled “Use the bearer token”](#use-the-bearer-token) Send the token on every core API call: ```http Authorization: Bearer ``` A quick way to confirm the token works: ```bash curl -fsS "https://apid.dustid.io/api/v1/me" \ -H "Authorization: Bearer $DUST_TOKEN" ``` Requests without a valid token get `401` with body `{ "code": "UNAUTHORIZED", "message": "...", "status": 401 }` — see [Request conventions](/api/conventions/) for the error contract. ## Token expiry and refresh [Section titled “Token expiry and refresh”](#token-expiry-and-refresh) Service Account bearer tokens are short-lived — currently 15 minutes, but always read the lifetime from the response (`expiresIn`/`expiresAt` on the key exchange, `expires_in` on the OAuth grant) rather than hardcoding it. There is no refresh token: when a token expires, exchange the credential again. A robust client combines both patterns — refresh proactively with a safety margin, and treat one `401` as a signal to refresh and retry (this also covers clock skew and mid-lifetime revocation): ```ts let cached: { token: string; refreshAfter: number } | null = null; async function getToken(): Promise { if (cached && Date.now() < cached.refreshAfter) return cached.token; const res = await fetch("https://apid.dustid.io/api/auth/token", { headers: { "x-api-key": process.env.DUST_API_KEY! }, }); if (!res.ok) throw new Error(`token exchange failed: ${res.status}`); const { token, expiresIn } = await res.json(); // refresh 60s before expiry, never cache a token for less than 5s cached = { token, refreshAfter: Date.now() + Math.max(expiresIn - 60, 5) * 1000 }; return token; } async function apiFetch(url: string, init: RequestInit = {}): Promise { const call = async () => { // new Headers() handles every HeadersInit shape (plain object, Headers, // tuple array) — an object spread would silently drop the latter two. const headers = new Headers(init.headers); headers.set("Authorization", `Bearer ${await getToken()}`); return fetch(url, { ...init, headers }); }; let res = await call(); if (res.status === 401) { cached = null; // token revoked or expired early — refresh once and retry res = await call(); } return res; } ``` The exchange is cheap; do not build long caches around it. The short lifetime is also your incident story: revoking a credential stops new tokens immediately, and any already-issued token dies within minutes. Long-running jobs A batch job that runs longer than a token’s lifetime must refresh mid-run — mint the token per request (as above), not once at startup. ## Declared actor attribution [Section titled “Declared actor attribution”](#declared-actor-attribution) A Service Account authenticates the *system*; it cannot tell DUST which *person* pressed the button in your ERP or on your shop floor. If you want that traceability, declare it per request with the `Dust-Ctx-Declared-Actor` header — a small JSON object: ```http Dust-Ctx-Declared-Actor: {"id": "JDOE", "system": "SAP", "displayName": "Jane Doe"} ``` * `id` is required; `system`, `displayName`, and `role` are optional. The value may be URI-encoded (required if it contains non-ASCII characters) and must stay under 1 KB. * The declared actor is recorded verbatim on every event the request writes and shown in activity history as *declared* attribution — it is supplied by your integration, not verified by DUST, and it never grants or restricts permissions. * An organization admin can set a Service Account’s attribution policy to **required**, in which case write requests without a declared actor are rejected with `403 ATTRIBUTION_REQUIRED`. ## How tokens are verified [Section titled “How tokens are verified”](#how-tokens-are-verified) The API verifies each bearer token’s signature against AuthD’s JSON Web Key Set and checks the issuer (`https://authd.dustid.io/api/auth` in production) and audience claims. You normally never need this detail — but if your own backend wants to verify DUST-issued JWTs (for example, to trust a token forwarded from another internal service), the JWKS is public: ```text GET https://apid.dustid.io/api/auth/jwks ``` It returns a standard `{ "keys": [ ... ] }` document usable with any JOSE library. ## Keep credentials server-side [Section titled “Keep credentials server-side”](#keep-credentials-server-side) Never ship credentials to browsers or mobile apps A Service Account credential is long-lived and acts with all of the Service Account’s access. Anything embedded in page JavaScript, a mobile binary, or a public repo must be treated as leaked. * **Credentials live only on your servers** — environment variables or a secrets manager, never in client bundles, never in source control. * **Bearer tokens are also credentials.** They are short-lived, but a token minted from your credential acts with the Service Account’s full access. Don’t embed them in shipped clients either. * **If your web or mobile app needs to call the DUST API**, put a small backend in front of it: the backend holds the credential, mints bearer tokens, and either proxies the API calls or hands the browser a short-lived token for the session. Scanner and mobile integrations follow this same pattern — captures go to *your* backend, which calls the DUST API with server-held credentials. * **One Service Account per application/environment** makes rotation, revocation, and audit surgical. ## Rotate a key [Section titled “Rotate a key”](#rotate-a-key) Multiple credentials can be active on one Service Account at the same time, so rotation never needs downtime: 1. Create a replacement key (or OAuth client) on the same Service Account. 2. Deploy it to your application (both credentials work during the overlap). 3. Confirm production traffic uses the new credential — each key’s last-used time is visible in the portal. 4. Revoke the old credential. ## Next steps [Section titled “Next steps”](#next-steps) * [API quickstart](/api/quickstart/) — token to first thread in five minutes. * [Request conventions](/api/conventions/) — the context headers every org-scoped call needs. * [Full API reference](/reference/api/) — every endpoint and schema. # Compatibility and versioning > What you can rely on when you build against the DUST API — what changes without notice, what never will, and how deprecations work. The DUST API is versioned in the path — every endpoint lives under `/api/v1`. Within a version, we evolve the API continuously, but along strict rules: changes are **additive by default**, anything that would break a well-behaved integration goes through a deprecation window first, and the [published OpenAPI specification](/openapi.json) is the authoritative statement of the contract at any moment. This page defines what “well-behaved” means for your integration and what we promise in return. ## What may change without notice [Section titled “What may change without notice”](#what-may-change-without-notice) These changes are considered backward compatible. They can appear in any release, and your integration must tolerate them: * **New endpoints** and new operations on existing paths. * **New optional request parameters, headers, and body fields.** Existing requests keep working unchanged. * **New fields in responses.** Objects grow over time. * **New values in enumerated fields** — new event types, states, and kinds are added as the product grows. * **New error codes** for failure modes that previously surfaced as a generic code. * **Documentation, error `message` text, and field ordering.** Human-readable strings are not contract; JSON member order is never significant. ## Writing an integration that stays compatible [Section titled “Writing an integration that stays compatible”](#writing-an-integration-that-stays-compatible) The rules above are safe if your client follows standard tolerant-reader practice: * **Ignore response fields you don’t recognize.** Never fail on unexpected members, and don’t use strict schema validation that rejects unknown fields. * **Tolerate unknown enum values.** Branch on the values you handle and fall through cleanly on ones you don’t. * **Branch on error `code`, never on `message`.** Codes are stable identifiers; messages are localized and can be reworded. See [Request conventions](/api/conventions/#errors). * **Treat IDs and pagination cursors as opaque strings.** Persist and replay them; never parse or construct them. * **Call only what the published specification documents.** Endpoints, fields, and behaviors not in the [public OpenAPI spec](/openapi.json) carry no compatibility promise. An integration that follows these rules is unaffected by additive change and is what the promises below protect. ## What we treat as breaking [Section titled “What we treat as breaking”](#what-we-treat-as-breaking) We do not do any of the following to a published `/api/v1` operation without the deprecation process described below: * Removing or renaming an endpoint, request parameter, or response field. * Changing a field’s type or format. * Making an optional request input required, or narrowing the values an input accepts. * Removing a value from an enumerated field. * Changing the error `code` or HTTP status returned for an existing, documented failure mode. * Requiring a higher [authorization tier](/api/conventions/#authorization-tiers) or new permission for an existing operation. * Materially changing an operation’s semantics, even if its shape is unchanged. ## Deprecation [Section titled “Deprecation”](#deprecation) When we do need to retire or reshape something, it is deprecated first: * The operation or field is marked `deprecated: true` in the published OpenAPI specification, and the deprecation is noted in these docs. * Deprecated functionality keeps working, unchanged, for **at least 90 days** from the announcement. * A documented replacement is available before or at the moment of deprecation whenever one exists. Watch the spec, not the wire The published OpenAPI specification at [`/openapi.json`](/openapi.json) always reflects the current contract, including deprecation markers. Diffing it between your integration reviews is the most reliable way to see what changed. ## Versioning [Section titled “Versioning”](#versioning) New major versions are rare by design. `/api/v1` evolves additively; we would introduce a `/api/v2` only for a reshape that cannot be expressed compatibly, and `/api/v1` would then remain supported through a long, explicitly announced migration period — never removed on the deprecation window above. ## See also [Section titled “See also”](#see-also) * [Request conventions](/api/conventions/) — the shared request contract these promises apply to. * [Authentication and API keys](/api/authentication/) — service accounts and token lifetimes. * [Full API reference](/reference/api/) — generated from the published specification. # Request conventions: context headers, errors, localization > The headers, error shape, pagination, and localization rules shared by every DUST API endpoint. Every org-scoped `/api/v1/*` endpoint shares the same request contract: a bearer token, two context headers that pick the organization and team you act in, JSON bodies (file uploads use multipart or tus instead), one error shape, and cursor pagination on list endpoints. This page is the contract; per-domain pages assume it. ## Context headers [Section titled “Context headers”](#context-headers) Almost everything in the DUST API belongs to an organization, and within it a **team**. You choose which org/team a request acts in with two headers: | Header | Required | Value | | ------------------ | ---------------------------- | ----------------------------------------------------------------- | | `Dust-Ctx-Org-Id` | Yes, on org-scoped endpoints | Organization UUID. | | `Dust-Ctx-Team-Id` | No | Team UUID. Defaults to the organization’s root team when omitted. | ```bash curl -fsS "https://apid.dustid.io/api/v1/threads" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \ -H "Dust-Ctx-Team-Id: $DUST_TEAM_ID" ``` Grp is the legacy spelling `Dust-Ctx-Grp-Id` is the legacy name for `Dust-Ctx-Team-Id` and is still accepted everywhere the new header is (when both are sent, `Dust-Ctx-Team-Id` wins). A few error codes (`GROUP_ID_REQUIRED`) and field names also predate the group→team rename — always read `Grp`/`group` in the wire protocol as “team”. Details that matter in practice: * Header values must be UUIDs; a malformed value is rejected with `400 INVALID_REQUEST` before the endpoint runs. * `X-`-prefixed variants (`X-Dust-Ctx-Org-Id`, `X-Dust-Ctx-Team-Id`, and the legacy pair) are accepted as aliases. * Endpoints that require context but don’t receive it fail with error codes `ORG_ID_REQUIRED` or `TEAM_ID_REQUIRED`. * A few endpoints are user-scoped and need no context — `GET /api/v1/me` is the common one. ### Context is an authorization boundary [Section titled “Context is an authorization boundary”](#context-is-an-authorization-boundary) Authorization is evaluated for *your user acting in the team named by the headers*. The same call with a different `Dust-Ctx-Team-Id` can return different results: what you can list, read, and write is what that team can see — its own records plus whatever has been shared with it. Sending a context you don’t belong to doesn’t escalate anything; requests are checked against your actual memberships. See [Teams and sharing](/api/teams-and-sharing/). ## Localization [Section titled “Localization”](#localization) The optional `Dust-Ctx-Locale` header selects the language for server-generated, user-facing text — most visibly error `message` strings: ```http Dust-Ctx-Locale: zh-CN ``` Supported locales are `en` (default) and `zh-CN`. When the header is absent the server falls back to the standard `Accept-Language` header, then to English. Error **codes** are stable identifiers and never localized — branch on `code`, display `message`. ## Errors [Section titled “Errors”](#errors) Failed requests return a JSON body with a single, consistent shape: ```json { "code": "UNAUTHORIZED", "message": "You are not authorized to perform this action", "status": 401, "detail": { } } ``` | Field | Type | Meaning | | --------- | ----------------- | ------------------------------------------------------------ | | `code` | string | Stable, machine-readable error code. Branch on this. | | `message` | string | Human-readable description, localized per `Dust-Ctx-Locale`. | | `status` | number | Mirrors the HTTP status code. | | `detail` | object (optional) | Extra context for this error, e.g. validation specifics. | Codes you will encounter early: | Code | Typical status | When | | -------------------------------------- | -------------- | ------------------------------------------------------------------- | | `INVALID_REQUEST` | 400 | Malformed body, query, or header (validation detail in `detail`). | | `UNAUTHORIZED` | 401 | Missing, expired, or invalid bearer token. | | `FORBIDDEN` | 403 | Authenticated, but this team context may not do that. | | `NOT_FOUND` / `NO_DATA_FOUND` | 404 | No such record visible in this context. | | `ORG_ID_REQUIRED` / `TEAM_ID_REQUIRED` | 400 | Context header missing on a scoped endpoint. | | `THREAD_DATA_CONFLICT` | 409 | Optimistic-concurrency conflict: your view of the thread was stale. | Every response also carries an `x-request-id` header. Log it, and include it when contacting support — it pinpoints your request in server traces. ## Pagination [Section titled “Pagination”](#pagination) List endpoints (threads, bundles, files, events, templates, …) use cursor pagination: * Request: `pageSize` (page length) and `cursor` (opaque string from a previous page) query parameters. * Response: the items array plus optional `next` and `prev` cursor strings. A missing `next` means you’re on the last page. ```bash # First page curl -fsS "https://apid.dustid.io/api/v1/threads?pageSize=50" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" # Follow the cursor curl -fsS "https://apid.dustid.io/api/v1/threads?pageSize=50&cursor=$NEXT" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" ``` ```json { "threads": [ ... ], "next": "eyJjcmVhdGVkQXQiOi...", "prev": "eyJjcmVhdGVkQXQiOi..." } ``` Cursors are opaque — persist and replay them, never parse them. List endpoints that support ordering take `order` (`asc`/`desc`) and an endpoint-specific `orderCol` (for threads: `createdAt`, `updatedAt`, `name`). ## Authorization tiers [Section titled “Authorization tiers”](#authorization-tiers) Every operation requires one of three privilege levels, and the path prefix tells you which before you read a single schema: | Prefix | Who can call it | Notes | | ----------------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | | `/api/v1/org/*` | **Organization admins** — users with the `admin` (or `owner`) role in the Organization named by `Dust-Ctx-Org-Id` | Team creation, team updates, memberships | | `/api/v1/connections/*` | **Team admins** — admins of the acting Team named by `Dust-Ctx-Team-Id` | Connection lifecycle and amendments | | everything else | **Members** of the request context, unless the operation says otherwise | Standard feature surface | Each operation also carries an `x-required-role` extension in the OpenAPI spec (`member`, `publisher`, `team-admin`, or `org-admin`) — treat that as the authoritative per-operation policy; an operation without the annotation requires `member`. `publisher` is a grant on a Team membership rather than a tier of its own: it is required to make a Team’s data publicly readable, and Team admins always have it. Calling an operation above your tier returns `403 FORBIDDEN` regardless of payload. ## Bodies, IDs, and timestamps [Section titled “Bodies, IDs, and timestamps”](#bodies-ids-and-timestamps) * **Requests** are `Content-Type: application/json` unless an endpoint explicitly takes multipart form data (identifier scans on `/api/v1/tags/*`, file uploads). * **IDs** are RFC 4122 UUID strings (`threadId`, `eventId`, organization and team ids, …). Treat them as opaque. * **Timestamps** (`createdAt`, `updatedAt`, `archivedAt`, …) are UTC timestamp strings. * **Writes are evented**: mutating a thread appends to its event history rather than silently overwriting — reads like `GET /api/v1/threads/{thread_id}` return `{ thread, events }`. ## See also [Section titled “See also”](#see-also) * [API quickstart](/api/quickstart/) — these conventions in one working flow. * [Authentication and API keys](/api/authentication/) — where the bearer token comes from. * [Core model](/api/core-model/) — what threads, teams, and identifiers mean. * [Full API reference](/reference/api/) — per-endpoint parameters and schemas, generated from the live spec. # Core model > The domain objects behind every DUST API call, and where each one lives in the API surface. The DUST platform API models physical-object workflows as a small set of composable resources. A **Thread** is the digital record for a physical item; everything else — identifiers, files, folders, assemblies, sharing, shipments — attaches to, organizes, or moves Threads. This page is the map: one short section per concept, with the key endpoints and a link to the deeper guide. ## Name map [Section titled “Name map”](#name-map) Some API namespaces predate the current product vocabulary. The DICE web app and these docs use the left-hand names; the API paths keep the names on the right. | DICE / docs name | API namespace | Notes | | -------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | Threads | `/api/v1/threads` | — | | Identifiers | `/api/v1/tags` | Legacy `tags` naming in paths | | Files | `/api/v1/files` | Called *resources* in some schemas | | Folders & Categories | `/api/v1/bundles` | *Bundle* is the implementation name | | Assemblies | `/api/v1/assemblies` | Assemblies are Threads of kind `assembly` | | Teams | `/api/v1/teams` | Selected per-request via the `Dust-Ctx-Team-Id` header (legacy `Dust-Ctx-Grp-Id` still accepted) | | Connections | `/api/v1/connections` | Wire schemas keep the legacy *team link* naming | | Sharing | `/api/v1/sharing` | — | | Shipments | `/api/v1/transfers` | Legacy `transfers` naming in paths | | Slices | `/api/v1/slices` | — | | Fabric | `/api/v1/fabric` | Cross-organization provenance graph | | Certificates | `/api/v1/certificates`, `/api/v1/certificate-forms` | — | | Public Pages | `/api/v1/public-pages`, `/api/v1/public-page-designs` | Publishing requires the Team `publisher` grant | | Events | `/api/v1/events` | — | Every request carries an AuthD bearer token; org-scoped endpoints — nearly all of them — add the `Dust-Ctx-Org-Id` header (and optionally `Dust-Ctx-Team-Id` to select a Team). See [Authentication](/api/authentication/) and [Conventions](/api/conventions/). The complete parameter-level reference is the [API reference](/reference/api/). ## Threads [Section titled “Threads”](#threads) A Thread is the record for one physical asset, part, document, or workflow item: a name and description, typed field data, attached files, bound identifiers, and an event history. Threads have a `kind` — ordinary units or `assembly` (see below). * `POST /api/v1/threads` — create one or many * `GET /api/v1/threads` — search and list (cursor pagination) * `GET /api/v1/threads/{thread_id}` — get one, with field data * `POST /api/v1/threads/{thread_id}/data` — upsert or remove field values * `PATCH /api/v1/threads/archive` / `PATCH /api/v1/threads/restore` — archive lifecycle Deep dive: [Threads API guide](/api/threads/). ### Fields and Templates [Section titled “Fields and Templates”](#fields-and-templates) Field values are typed (`text`, `number`, `date`, `select`, resource references, even thread-valued fields) and nested by type. **Templates** define the expected fields for a repeatable kind of Thread. * `POST /api/v1/templates` / `GET /api/v1/templates` — create and list templates * `GET /api/v1/templates/{templateId}` / `PATCH /api/v1/templates/{templateId}` — read and update ## Identifiers [Section titled “Identifiers”](#identifiers) An identifier binds a physical marking — a DUST tag, QR code, barcode, Data Matrix symbol, or NFC chip — to a Thread, so a scan in the field resolves to the digital record. The API namespace is `/api/v1/tags` (legacy naming). * `POST /api/v1/tags/extract` — parse a DUST capture into a canonical fingerprint without binding * `POST /api/v1/tags/bind` — attach an identifier to a Thread * `POST /api/v1/tags/identify` — find the Thread matching a scan * `POST /api/v1/tags/verify` — confirm a scan matches a specific Thread’s identifiers * `POST /api/v1/tags/unbind` — detach an identifier Deep dive: [Identifiers API guide](/api/identifiers/). ## Files [Section titled “Files”](#files) Files (called *resources* in some schemas) are stored in object storage and attached to Threads directly or through resource-typed fields. Large uploads use the resumable tus protocol; small ones use a single multipart POST. * `POST /api/v1/files` — simple multipart upload * `POST /api/v1/files/finalize` — turn completed tus uploads into resource records * `GET /api/v1/files/{resource_id}/download` — download * `POST /api/v1/files/urls` — short-lived signed URLs * `GET /api/v1/files/search` — search across files Deep dive: [Files API guide](/api/files/). ## Teams and Organizations [Section titled “Teams and Organizations”](#teams-and-organizations) Identity lives in AuthD; the platform API scopes each org-scoped request to an Organization and a Team via context headers. Teams own Threads, and sharing, connections, and shipments all operate between Teams. * `GET /api/v1/me` — current user and available organizations * `GET /api/v1/teams` — Teams visible to the caller * `POST /api/v1/org/teams` / `PATCH /api/v1/org/teams/{team_id}` — Team management (org admins) * `POST /api/v1/org/teams/members` — manage memberships (org admins) Deep dive: [Teams, sharing, and connections](/api/teams-and-sharing/). ## Folders and Categories [Section titled “Folders and Categories”](#folders-and-categories) Folders and Categories organize Threads. Both are *bundles* in the API — `kind: "folder"` for exclusive containment, `kind: "category"` for non-exclusive labeling — and bundles nest to form trees. * `POST /api/v1/bundles` — create (with `kind` and optional `childOfId` parent) * `GET /api/v1/bundles` / `GET /api/v1/bundles/children` — list, or walk the tree lazily * `POST /api/v1/bundles/{bundle_id}/add` / `PATCH /api/v1/bundles/{bundle_id}/move` — place Threads * `PATCH /api/v1/bundles/parent` — re-parent a bundle ## Assemblies and Parts [Section titled “Assemblies and Parts”](#assemblies-and-parts) An assembly is a Thread of kind `assembly` whose **Parts** are other Threads — a bill-of-materials structure. Parts can be protected against detachment, and part lists roll up transitively. * `GET /api/v1/assemblies` — list assembly Threads * `POST /api/v1/assemblies/{assembly_id}/parts` / `DELETE /api/v1/assemblies/{assembly_id}/parts` — attach and detach Parts * `GET /api/v1/assemblies/{assembly_id}/rolled-up-parts` — transitive part list * `PATCH /api/v1/assemblies/{assembly_id}/kind` — convert a Thread between `unit` and `assembly` * `POST /api/v1/imports/plan` / `POST /api/v1/imports/commit` — dry-run and commit a whole assembly import package ## Thread Links and Relationships [Section titled “Thread Links and Relationships”](#thread-links-and-relationships) Threads can reference each other with typed links. **Relation definitions** name the relationship kinds; **thread links** are the instances. * `POST /api/v1/relations` / `GET /api/v1/relations` — define and list relation kinds * `POST /api/v1/links` / `GET /api/v1/links` — create and list links between Threads * `GET /api/v1/threads/{thread_id}/links` — links from one Thread’s perspective * `DELETE /api/v1/links/{link_id}` — unlink ## Sharing [Section titled “Sharing”](#sharing) Sharing grants another Team `viewer` or `editor` access to a Thread or bundle. Grants are stored as relationship tuples; the access summary shows the effective result, including inherited access. * `POST /api/v1/sharing` — share Threads or bundles with Teams * `GET /api/v1/sharing` — list grants (`direction=in|out`) * `GET /api/v1/sharing/access-summary` — effective access for one object * `GET /api/v1/sharing/partner-inventory` — everything shared with one partner Team Deep dive: [Teams, sharing, and connections](/api/teams-and-sharing/). ## Connections [Section titled “Connections”](#connections) A Connection (API: *team link*) is the standing agreement between two Teams — often in different Organizations — that permits sharing and shipments, with an allowed data-flow direction. It has an invite/accept/confirm handshake and a pause/resume lifecycle. * `POST /api/v1/connections` — create (invite) * `PATCH /api/v1/connections/accept` / `confirm` / `reject` / `cancel` — handshake * `PATCH /api/v1/connections/pause` / `resume` — suspend and restore * `POST /api/v1/connections/amend/propose` — propose a direction change ## Shipments [Section titled “Shipments”](#shipments) A Shipment (API: *transfer*) moves ownership of Threads from one Team to another: build a draft manifest, send it, and the receiver accepts, rejects, or requests changes. * `POST /api/v1/transfers` — create a draft * `POST /api/v1/transfers/{transfer_id}/items` — add manifest items * `POST /api/v1/transfers/{transfer_id}/send` — send to the receiving Team * `POST /api/v1/transfers/{transfer_id}/respond` — accept / reject / request changes * `GET /api/v1/transfers` — inbox, outbox, and sent views Semantics and lifecycle: [Shipments](/use/shipments/); endpoint summary in [Teams, sharing, and connections](/api/teams-and-sharing/). ## Slices [Section titled “Slices”](#slices) A Slice derives a new Thread from an existing one within the same Team — selected fields, files, and identifiers copied or linked — typically to prepare a shareable subset. * `POST /api/v1/slices` — slice one Thread * `POST /api/v1/slices/batch` — derive many Threads at once * `GET /api/v1/slices/{slice_id}` — a Slice with its Fabric links ## Fabric [Section titled “Fabric”](#fabric) Fabric is the cross-organization provenance layer: when Threads move or are disclosed across Team boundaries, Fabric records the graph of linked Threads and controls exactly which data each downstream party can see (disclosure), revision by revision. * `GET /api/v1/fabric/threads/{thread_id}/graph` — the provenance graph visible from a Thread * `GET /api/v1/fabric/links/{link_id}/context` — currently disclosed data on a link * `POST /api/v1/fabric/threads/{thread_id}/disclosure/revise` / `redact` — change what is disclosed * `POST /api/v1/fabric/threads/{thread_id}/disclosure/push` — push a disclosure downstream * `GET /api/v1/fabric/notifications` — disclosure notifications for downstream owners Concepts: [Fabric](/use/fabric/). ## Certificates [Section titled “Certificates”](#certificates) Certificates render Thread data into issued, verifiable documents. **Certificate Forms** are the layouts; generation binds a form to a Thread by field name. A form may contain multiple Vlink QR zones. Certificate generation accepts one Vlink configuration per zone Identifier and returns every issued zone-to-Vlink association. * `POST /api/v1/certificate-forms` / `GET /api/v1/certificate-forms` — manage forms * `POST /api/v1/certificates/preflight` — check a form resolves against a Thread * `POST /api/v1/certificates/generate` — issue a Certificate * `GET /api/v1/certificates` — list a Thread’s Certificates * `POST /api/v1/certificates/void` — void one Concepts: [Certificates](/use/certificates/). ## Public Pages [Section titled “Public Pages”](#public-pages) A **Public Page** is the unauthenticated web view of a Thread — the digital product passport a consumer reaches by scanning an Identifier. What it shows is decided entirely by a reusable, Team-owned **Public Page Design**, so publishing takes no per-Thread content input: publish resolves the design against the Thread. A page’s URL is reserved and bound before anything is published, so labels can be printed first. Publishing a design freezes an immutable **Design Version**; each page pins one Design Version plus one **Data Snapshot** (the values resolved for that Thread). A **Publish Wave** republishes every page in a scope — Folder, Category, Template, or an explicit selection — through one Design Version, as a background run with its own progress and failure accounting. Reserving a page URL and binding it to a Thread are member-tier — reserving an address publishes nothing, so labels can be printed before anyone decides to publish. Everything that makes data public — publishing a page, activating or archiving it, authoring a design, publishing a Design Version, rolling out, and Publish Waves — requires the Team `publisher` grant (Team admin implies it), as do the preflight and preview checks. Each operation’s `x-required-role` in the [API reference](/reference/api/) is authoritative. * `POST /api/v1/public-pages` / `POST /api/v1/public-pages/{publicPageId}/bind` — reserve a permanent page URL, then bind it to a Thread * `GET` / `PUT /api/v1/public-pages/thread/{threadId}` — read, or get-or-create, a Thread’s page * `GET /api/v1/public-pages/thread/{threadId}/activity` — anonymous views and verification scans on the published page * `POST /api/v1/public-pages/{publicPageId}/publish` — publish a snapshot through the design’s latest version * `PATCH /api/v1/public-pages/{publicPageId}` — activate or archive a page without changing its URL * `GET /api/v1/public-pages/{publicPageId}/publications` — publication history * `POST /api/v1/public-pages/preflight` / `preflight/batch` — check a design resolves against one or many Threads * `POST /api/v1/public-page-designs` / `GET` / `PATCH /api/v1/public-page-designs/{designId}` — author a design draft * `POST /api/v1/public-page-designs/{designId}/versions` — publish a Design Version (`GET` lists them) * `POST /api/v1/public-pages/designs/{designId}/roll-out` — make a design’s latest version live across its pages * `POST /api/v1/public-pages/waves` — start a Publish Wave (`GET` its run record, items, and list) * `POST /api/v1/public-pages/waves/{waveId}/retry-failed` / `cancel` — retry the failures, or stop the remaining work Rollout is forward-only: a Design Version is never restored, and cancelling a wave leaves already-published pages on the version they received. Concepts: [Public Pages](/use/public-pages/). ## Events [Section titled “Events”](#events) Every meaningful change — field edits, binds, shares, shipments — is recorded as an event, forming the audit trail shown as Transaction History in DICE. * `GET /api/v1/events` — list events, filterable by Thread, Team, action, and time, with optional activity grouping (`groupBy`) * `GET /api/v1/summary` — headline count metrics * `GET /api/v1/notifications` — the caller’s notifications ## Where to go next [Section titled “Where to go next”](#where-to-go-next) Quickstart Make your first authenticated call in [the quickstart](/api/quickstart/). TypeScript client Use the typed [`@dustid/apid-client`](/api/typescript-client/) instead of raw HTTP. Conventions Headers, pagination, and errors in [API conventions](/api/conventions/). Full reference Every path, parameter, and schema in the [API reference](/reference/api/). # Files API guide > Upload files with tus resumable uploads or simple POSTs, then download, search, and verify them. Files (called **resources** in some schemas) hold the evidence attached to [Threads](/api/threads/): images, PDFs, documents, and scan artifacts. A file can be attached to a Thread directly or as the value of a resource-typed field. There are two upload paths: a simple one-shot POST for small files, and the resumable [tus protocol](https://tus.io) for large ones. Full schemas: [API reference](/reference/api/). ## Endpoints at a glance [Section titled “Endpoints at a glance”](#endpoints-at-a-glance) | Operation | Method & path | | ---------------------- | ------------------------------------------------------------------- | | Simple upload | `POST /api/v1/files` | | Resumable upload (tus) | `POST /api/v1/files/upload`, then `PATCH /api/v1/files/upload/{id}` | | Finalize tus uploads | `POST /api/v1/files/finalize` | | Download | `GET /api/v1/files/{resource_id}/download` | | Signed URLs | `POST /api/v1/files/urls` | | Search files | `GET /api/v1/files/search` | | List files (cursor) | `GET /api/v1/files` | | List a Thread’s files | `GET /api/v1/threads/{thread_id}/files` | ## Simple upload [Section titled “Simple upload”](#simple-upload) For files a single request can carry comfortably, POST `multipart/form-data` with the `file` and optional attachment targets. The response includes the created resource with a signed URL: * curl ```bash curl -fsS "$APID_URL/api/v1/files" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \ -F "file=@inspection-report.pdf" \ -F "threadId=$THREAD_ID" ``` * TypeScript ```ts const resources = await client.files.upload({ file, // a File threadId, // optional: attach to a Thread // fieldId, // optional: attach as a field value // isPrivate: true, // optional }); ``` `fieldId` attaches the file as the value of a resource-typed field; `isPrivate` restricts visibility. ## Resumable upload (tus) [Section titled “Resumable upload (tus)”](#resumable-upload-tus) Large files use the tus 1.0 resumable-upload protocol at `/api/v1/files/upload`, then a **finalize** call that turns the completed upload into a resource record. The flow is upload → finalize → (already attached, or attach via fields). 1. **Create the upload.** POST with tus headers; `Upload-Metadata` values are base64-encoded, and `filename` is required (optional `fieldId`, `threadId`): ```bash curl -i -X POST "$APID_URL/api/v1/files/upload" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \ -H "Tus-Resumable: 1.0.0" \ -H "Upload-Length: 52428800" \ -H "Upload-Metadata: filename $(printf 'video.mp4' | base64)" # → 201 Created # → Location: …/api/v1/files/upload/ ``` The `` in the returned `Location` is the upload’s **resource ID** — keep it. 2. **Send the bytes** (resumable — repeat PATCHes continue from `Upload-Offset`): ```bash curl -i -X PATCH "$APID_URL/api/v1/files/upload/$UPLOAD_ID" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \ -H "Tus-Resumable: 1.0.0" \ -H "Upload-Offset: 0" \ -H "Content-Type: application/offset+octet-stream" \ --data-binary @video.mp4 ``` To resume after an interruption, `HEAD` the same URL to read the current `Upload-Offset`, then PATCH from there. Any tus 1.0 client library (e.g. `tus-js-client`) speaks this protocol for you. 3. **Finalize.** Uploads become resource records only after finalize: ```bash curl -fsS "$APID_URL/api/v1/files/finalize" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \ -H "Content-Type: application/json" \ -d '{ "threadId": "'"$THREAD_ID"'", "requests": [ { "resId": "'"$UPLOAD_ID"'", "filename": "video.mp4", "size": 52428800 } ] }' ``` `POST /api/v1/files/finalize` accepts many uploads at once; each request item takes the tus upload ID as `resId` plus `filename` and `size` (optional `fieldId`, `isPrivate`). The response returns the created resources with signed URLs. Note The tus endpoints speak the tus wire protocol rather than JSON, so they appear only summarily in the [API reference](/reference/api/). The behavior above is defined by the server’s tus configuration (`/upload` and `/upload/{id}` under `/api/v1/files`). ## Downloads and signed URLs [Section titled “Downloads and signed URLs”](#downloads-and-signed-urls) * `GET /api/v1/files/{resource_id}/download` — stream the file through the API. * `POST /api/v1/files/urls?ids=&ids=` — mint short-lived signed URLs for direct object-storage access; use these when a browser or downstream system needs the bytes without proxying. Caution Signed URLs expire. Store resource IDs, never signed URLs — re-request URLs when you need fresh access. ## Search and listing [Section titled “Search and listing”](#search-and-listing) Two query styles exist: * `GET /api/v1/files/search` — page-indexed search with `q`, `threadId`, `mimeFilters`, and `includeArchived`. * `GET /api/v1/files` — cursor-paginated listing (`cursor`, `pageSize`; `includeArchived` is required) filtered by `threadId` or `createdBy`. For files in the context of one Thread, prefer `GET /api/v1/threads/{thread_id}/files` (see the [Threads guide](/api/threads/)). ## Related pages [Section titled “Related pages”](#related-pages) * [Threads API guide](/api/threads/) — attaching files to Thread fields and thumbnails * [Core model](/api/core-model/) — where files sit in the domain * [API reference](/reference/api/) — full parameter and schema detail # Identifiers API guide > Extract, bind, identify, verify, and manage the physical identifiers attached to Threads. An **identifier** connects a physical marking to a [Thread](/api/threads/): a DUST tag, QR code, barcode, Data Matrix symbol, or NFC chip. Once bound, a scan in the field resolves to the digital record. The API namespace is `/api/v1/tags` — legacy naming that survives in paths and schemas; these docs say *identifier* in prose. Full request/response schemas: [API reference](/reference/api/). ## Operations at a glance [Section titled “Operations at a glance”](#operations-at-a-glance) | Operation | Method & path | Semantics | | --------- | ---------------------------- | ------------------------------------------------------------------ | | Extract | `POST /api/v1/tags/extract` | Parse a DUST capture into a canonical fingerprint, without binding | | Bind | `POST /api/v1/tags/bind` | Associate an identifier with a Thread | | Identify | `POST /api/v1/tags/identify` | Search: which Thread matches this scan? | | Verify | `POST /api/v1/tags/verify` | Compare a scan against a *specific* Thread’s identifiers | | Unbind | `POST /api/v1/tags/unbind` | Detach an identifier from its Thread | | Set text | `POST /api/v1/tags/text` | Rename / re-describe a bound identifier | | Update | `POST /api/v1/tags/update` | Lifecycle: privacy, archive/restore (value and type are immutable) | **Identify vs. verify:** identify answers *“what is this?”* — it searches your visible Threads (optionally scoped by `searchTeamIds`) and returns the match, if any. Verify answers *“is this the item it claims to be?”* — you name a `threadId` and the candidate tag IDs, and the API confirms or denies. Use verify for authentication decisions; identify for lookup. ## Two payload families [Section titled “Two payload families”](#two-payload-families) Scan endpoints accept `multipart/form-data`, and the shape of `data` depends on the identifier type: | `tagType` | `data` | Where it comes from | | -------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------- | | `DUST` | An image — a binary file part or a base64 data URL (`data:image/jpeg;base64,…`) | A DUST optical capture from a scanner | | `QR`, `BAR_CODE`, `DATA_MATRIX`, `NFC` | The decoded string contents (or NFC hex ID) | Any symbol scanner | A DUST capture is a *photograph of the tag*, not a decoded value — the server extracts the fingerprint. Captures come from DUST scanning hardware: see [Integrate with DUST Go](/integrate/dust-go/) for mobile capture and the [React Scanner](/integrate/react-scanner/) for a drop-in web component that handles all modes. In multipart bodies, structured fields (`options`, `tags`, `searchTeamIds`) are passed as JSON strings. ## Extract a DUST capture [Section titled “Extract a DUST capture”](#extract-a-dust-capture) Extract parses a capture into a canonical fingerprint and returns its quality — useful for checking a capture before enrolling, or for staging a bind: ```bash curl -fsS "$APID_URL/api/v1/tags/extract" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \ -F "data=@scan.jpeg" \ -F 'options={"enrollmentSessionId":"3d5e…"}' ``` The response is `{ id, qualityScore, annotatedImage?, forensics? }` — `id` is a fingerprint ID you can later bind without re-uploading the image (below). `options` also carries capture metadata (device, optics, geolocation) that the platform stores with the scan. ## Bind an identifier to a Thread [Section titled “Bind an identifier to a Thread”](#bind-an-identifier-to-a-thread) `POST /api/v1/tags/bind` accepts three shapes, distinguished by `tagType` and payload: * DUST image ```bash curl -fsS "$APID_URL/api/v1/tags/bind" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \ -F "threadId=$THREAD_ID" \ -F "tagType=DUST" \ -F "tagDescription=Inbound receiving scan" \ -F "data=@scan.jpeg" \ -F 'options={"enrollmentSessionId":"3d5e…"}' ``` * Text identifier ```bash curl -fsS "$APID_URL/api/v1/tags/bind" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \ -F "threadId=$THREAD_ID" \ -F "tagType=QR" \ -F "data=https://example.com/item/SZ3J-11-ZJ17" ``` * DUST fingerprint ```bash # Reuse a fingerprint from a prior /extract — no image re-upload curl -fsS "$APID_URL/api/v1/tags/bind" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \ -F "threadId=$THREAD_ID" \ -F "tagType=DUST" \ -F "fingerprintId=$FINGERPRINT_ID" ``` DUST image binds group related captures with a client-generated `options.enrollmentSessionId` UUID, and can return the annotated capture (`options.returnAnnotatedImage: true`). ## Identify a Thread from a scan [Section titled “Identify a Thread from a scan”](#identify-a-thread-from-a-scan) * curl ```bash curl -fsS "$APID_URL/api/v1/tags/identify" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \ -F "tagType=DUST" \ -F "data=@scan.jpeg" \ -F 'searchTeamIds=["'"$TEAM_ID"'"]' ``` * TypeScript ```ts const result = await client.tags.identify({ tagType: "DUST", data: scanBlob, searchTeamIds: [teamId], }); ``` Identify also takes text payloads (`tagType` of `QR`/`BAR_CODE`/`DATA_MATRIX`/`NFC` with the decoded `data`) or a bare identifier ID (`tagType: "ANY"` with `tagId`). A hit returns the matched identifier and its Thread; a miss returns an unidentified result rather than an error. ## Verify a scan against a Thread [Section titled “Verify a scan against a Thread”](#verify-a-scan-against-a-thread) Verify is the authentication primitive: given a fresh scan, a `threadId`, and the candidate `tags` (identifier IDs bound to that Thread), it succeeds if any candidate matches. ```bash curl -fsS "$APID_URL/api/v1/tags/verify" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \ -F "threadId=$THREAD_ID" \ -F "tagType=DUST" \ -F "data=@scan.jpeg" \ -F 'tags=["'"$TAG_ID"'"]' ``` The response reports `success`, `attemptedCount` / `failedCount`, and on success the `verifiedTag`. ## Manage bound identifiers [Section titled “Manage bound identifiers”](#manage-bound-identifiers) These are plain JSON endpoints; all of them require both the `tagId` and the `threadId` the identifier is bound to: * `POST /api/v1/tags/text` — set `name` and/or `description`. * `POST /api/v1/tags/update` — set `name`, `description`, `isPrivate`, and `archivedAt` (an ISO timestamp archives the identifier; `null` restores it). The identifier’s value and type are immutable — rebind instead. * `POST /api/v1/tags/unbind` — detach the identifier from the Thread. Note To list a Thread’s identifiers, fetch the Thread itself — `GET /api/v1/threads/{thread_id}` includes its bound identifiers. You can also filter Thread listings by `tagType` (see the [Threads guide](/api/threads/)). ## Tamper Analysis [Section titled “Tamper Analysis”](#tamper-analysis) Under `/api/v1/tamper`, a **Tamper Analysis** compares a fresh scan of a DUST identifier against the reference captured when it was bound and records what it measured. The API returns measurements and evidence only — there is no summary number, band, threshold, or platform-authored result field anywhere in the surface, and `alignmentOutcome` reports solely whether the two scans could be compared at all (when they could not, the measurements are not comparable, which is not a statement about the identifier). | Operation | Method & path | | ------------------------ | --------------------------------------------------------------------------------------------------------------------- | | Run an Analysis | `POST /api/v1/tamper/analyses` — form-encoded: `threadId`, `tagId`, and exactly one of `data` or `queryFingerprintId` | | Record an Observation | `POST /api/v1/tamper/observations` — `{ analysisId, result }` | | List a Thread’s Analyses | `GET /api/v1/tamper/analyses?threadId=…` (optionally `tagId`, `limit`) | | Get one Analysis | `GET /api/v1/tamper/analyses/{analysis_id}` | | Fetch a result bitmap | `GET /api/v1/tamper/analyses/{analysis_id}/artifacts/{name}` | Running an Analysis takes a `multipart/form-data` or `application/x-www-form-urlencoded` body with `threadId`, `tagId`, and **exactly one** of: * `data` — the DUST scan itself, as a file or base64-encoded image. The service extracts it for you. * `queryFingerprintId` — a fingerprint ID you already hold from `POST /api/v1/tags/extract` (above), if you extracted separately. Sending both, or neither, is rejected. Either way an ordinary DUST capture is valid input — there is no separate capture path for tamper analysis. If the submitted scan cannot be read, the request fails and no Analysis is recorded. A **Tamper Observation** is the only conclusion the platform stores, and it is authored by a person: `result` is one of `consistent`, `expected`, `inconsistent`, or `unknown`, has no default, and is required. `expected` records normal wear and tear for the identifier’s use case and substrate. Observations are immutable and attributed; a new one never replaces an earlier one, and reads return the whole series (`observations`, newest first) rather than a single current result. Do not derive a result from the metrics or collapse the series to one value in your own UI. An Analysis carries `metrics` (a pass-through object of the algorithm’s coverage fractions and marker counts), optional `markerPoints`, and `artifactNames`. Marker coordinate sets are each in their own scan’s pixel space — compose them in one frame by applying `metrics.transformation_matrix` to the query points. Result bitmaps are protected content: fetch them through the artifact endpoint, which re-authorizes every request and returns non-cacheable bytes. Note Tamper Analysis is enabled per organization by DUST Identity. Where it is not available for an organization the API returns a `SCAN_ROUTING_UNAVAILABLE` error rather than a silent no-op. Full schemas are in the [API reference](/reference/api/); the end-user walkthrough is [Tamper Analysis](/use/tamper-analysis/). ## Related pages [Section titled “Related pages”](#related-pages) * [Integrate with DUST Go](/integrate/dust-go/) — capturing DUST scans on mobile * [React Scanner](/integrate/react-scanner/) — a copy-ready capture component * [Threads API guide](/api/threads/) — the records identifiers bind to * [API reference](/reference/api/) — full schemas, including capture metadata options # API quickstart > Exchange an API key for a bearer token, set your context headers, and create and read your first thread. This guide takes you from an API key to a thread you created and read back, entirely over HTTPS. Every request below runs against the DUST API at `https://apid.dustid.io`; see [Environments](/reference/environments/) for all service URLs. You will need a **Service Account API key** (created by an organization admin). If you don’t have one yet, start with [Authentication and API keys](/api/authentication/). 1. ### Exchange your API key for a bearer token [Section titled “Exchange your API key for a bearer token”](#exchange-your-api-key-for-a-bearer-token) API keys are never sent to core endpoints directly. Exchange the key for a short-lived bearer token at `GET /api/auth/token`, passing the key in the `x-api-key` header: * curl ```bash export APID_URL="https://apid.dustid.io" export DUST_API_KEY="your-service-account-key" export DUST_TOKEN="$( curl -fsS "$APID_URL/api/auth/token" \ -H "x-api-key: $DUST_API_KEY" | jq -r '.token' )" ``` * TypeScript ```ts const apidUrl = "https://apid.dustid.io"; const tokenResponse = await fetch(`${apidUrl}/api/auth/token`, { headers: { "x-api-key": process.env.DUST_API_KEY! }, }); const { token } = await tokenResponse.json(); ``` The response is a JSON object with a single `token` field — a JWT you send as `Authorization: Bearer ` on every `/api/v1/*` call. Tokens expire; re-run the exchange when they do. Details in [Authentication](/api/authentication/). 2. ### Find your organization and team context [Section titled “Find your organization and team context”](#find-your-organization-and-team-context) Most endpoints run inside an organization and team, named by two request headers. `GET /api/v1/me` returns the organizations your credentials can act in: ```bash curl -fsS "$APID_URL/api/v1/me" \ -H "Authorization: Bearer $DUST_TOKEN" ``` The response includes an `organizations` array (each with `id`, `name`, `slug`, `roles`) and an `activeOrganizationId`. Pick the organization you want to work in: ```bash export DUST_ORG_ID="" # Optional: export DUST_TEAM_ID="" if you want a non-root team. ``` | Header | Required | Purpose | | ------------------ | ----------------------------- | -------------------------------------------------- | | `Dust-Ctx-Org-Id` | Yes, for org-scoped endpoints | Organization UUID. | | `Dust-Ctx-Team-Id` | No | Team UUID. Defaults to the organization root team. | Grp means team `Dust-Ctx-Grp-Id`, the legacy spelling of `Dust-Ctx-Team-Id`, is still accepted — `Grp`/`group` is the legacy name for teams and survives in a few header and field names. See [Request conventions](/api/conventions/) for the full contract. 3. ### Create a thread [Section titled “Create a thread”](#create-a-thread) A thread is the record for one asset or item; its `data` array holds typed fields. `POST /api/v1/threads` with `type: "single"` creates one: * curl ```bash curl -fsS "$APID_URL/api/v1/threads" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \ -H "Content-Type: application/json" \ -d '{ "type": "single", "thread": { "name": "Tire SZ3J-11-ZJ17", "description": "Production asset" }, "data": [ { "name": "Serial Number", "type": "text", "value": { "text": "SZ3J-11-ZJ17" } }, { "name": "Max PSI", "type": "number", "value": { "number": 51 } } ] }' ``` * TypeScript ```ts import { ApidClient } from "@dustid/apid-client"; const client = new ApidClient({ baseUrl: apidUrl, bearerToken: token, organizationId, // sent as Dust-Ctx-Org-Id teamId, // optional; sent as Dust-Ctx-Team-Id when set }); // createOne unwraps the batch response to the single created thread const created = await client.threads.createOne({ type: "single", thread: { name: "Tire SZ3J-11-ZJ17", description: "Production asset", }, data: [ { name: "Serial Number", type: "text", value: { text: "SZ3J-11-ZJ17" } }, { name: "Max PSI", type: "number", value: { number: 51 } }, ], }); console.log(created.threadId); ``` The raw response is `201 Created` with `{ "created": [ ... ], "uploadResponses": [ ... ] }`; each entry in `created` is the full thread record, including its generated `threadId` (a UUID). Field `data` entries require `type` and `value`; `name` labels the field. 4. ### Read it back [Section titled “Read it back”](#read-it-back) `GET /api/v1/threads/{thread_id}` returns the thread plus its event history: * curl ```bash export THREAD_ID="" curl -fsS "$APID_URL/api/v1/threads/$THREAD_ID" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" ``` * TypeScript ```ts const record = await client.threads.get(created.threadId); console.log(record.thread.name); // "Tire SZ3J-11-ZJ17" console.log(record.events.length); // creation events already recorded ``` The response shape is `{ "thread": { ... }, "events": [ ... ] }` — every write to a thread is recorded as an event, so the audit trail starts at creation. 5. ### Where to go next [Section titled “Where to go next”](#where-to-go-next) * [Request conventions](/api/conventions/) — context headers, error shape, pagination, localization. * [Threads](/api/threads/) — field types, updates, archiving, listing and search. * [Identifiers](/api/identifiers/) — bind and verify physical identifiers against threads (the `/api/v1/tags/*` endpoints). * [Files](/api/files/) — attach evidence files to threads. * [Teams and sharing](/api/teams-and-sharing/) — cross-team access. * [TypeScript client](/api/typescript-client/) — the typed client used above. * [Full API reference](/reference/api/) — every endpoint, generated from the live OpenAPI spec. The server also self-hosts an interactive reference at `https://apid.dustid.io/api/docs` and the raw spec at `/api/openapi.json`. # Teams, sharing, and connections API guide > Request context, Team management, sharing grants, cross-organization Connections, and the Shipment endpoints. Everything in the DUST platform is owned and accessed by **Teams**. This guide covers the four layers that control who sees what: 1. **Context** — which Organization and Team a request acts as. 2. **Sharing** — granting another Team viewer or editor access to Threads and Folders. 3. **Connections** — the standing agreement between two Teams (usually across Organizations) that makes sharing and Shipments possible. 4. **Shipments and Slices** — moving or deriving records across those boundaries. Full schemas: [API reference](/reference/api/). ## Request context [Section titled “Request context”](#request-context) Identity lives in AuthD; the platform API scopes each call with headers: ```http Authorization: Bearer Dust-Ctx-Org-Id: Dust-Ctx-Team-Id: ``` `Dust-Ctx-Org-Id` is required for org-scoped calls. `Dust-Ctx-Team-Id` selects the acting Team and defaults to the Organization’s root Team (`Dust-Ctx-Grp-Id` is the accepted legacy spelling). See [Authentication](/api/authentication/) and [Conventions](/api/conventions/). * `GET /api/v1/me` — current user, session, active Organization, and available Organizations * `GET /api/v1/me/feature-flags` — feature flags for the caller ## Teams [Section titled “Teams”](#teams) Teams partition an Organization; Threads, Folders, and shares all belong to a Team. | Operation | Method & path | | ---------------------------------------------- | ---------------------------------------------- | | List Teams you can see | `GET /api/v1/teams` | | List connected partner Teams | `GET /api/v1/teams/connected` | | Create Teams (org admin) | `POST /api/v1/org/teams` | | List all Teams in the Organization (org admin) | `GET /api/v1/org/teams` | | Update / delete a Team (org admin) | `PATCH` / `DELETE /api/v1/org/teams/{team_id}` | | Add or update memberships (org admin) | `POST /api/v1/org/teams/members` | | List / remove memberships (org admin) | `GET` / `DELETE /api/v1/org/teams/members` | `GET /api/v1/teams` supports `q`, `role`, `rootId`, and `includeLinked` (to include connected partner Teams in pickers). `GET /api/v1/teams/connected` lists the partner Teams reachable through active Connections — the valid audience for sharing and Shipments. ## Sharing [Section titled “Sharing”](#sharing) A share grants one Team access to one object — a Thread or a bundle (Folder/Category) — as `viewer` or `editor`. Grants are stored as relationship tuples, and access can also arrive indirectly (a shared Folder conveys its contents), so there are two read models: the raw grant list, and the *effective* access summary. * curl ```bash curl -fsS "$APID_URL/api/v1/sharing" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \ -H "Dust-Ctx-Team-Id: $DUST_TEAM_ID" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "item": "thread", "id": "'"$THREAD_ID"'", "teamId": "'"$PARTNER_TEAM_ID"'", "relation": "viewer" } ] }' ``` * TypeScript ```ts await client.sharing.add({ items: [ { item: "thread", id: threadId, teamId: partnerTeamId, relation: "viewer" }, ], }); ``` | Operation | Method & path | | ---------------------------------- | ------------------------------------------------------------------------- | | Create shares | `POST /api/v1/sharing` | | List shares | `GET /api/v1/sharing?direction=in\|out` | | Update a share’s relation | `PATCH /api/v1/sharing/{tuple_id}` | | Remove shares | `DELETE /api/v1/sharing` (body: `{ "ids": […] }`) | | Effective access for one object | `GET /api/v1/sharing/access-summary?objectId=…&objectType=thread\|bundle` | | Everything shared with one partner | `GET /api/v1/sharing/partner-inventory?teamId=…` | `direction=out` lists what your Team has shared; `direction=in` what has been shared with it. The access summary resolves direct grants, Folder inheritance, and Team relationships into the effective permissions for an object; partner inventory is the per-Connection view — useful before pausing or amending a Connection. Thread-side conveniences: `GET /api/v1/threads/{thread_id}/shared` (who this Thread is shared with) and `POST /api/v1/threads/permissions` (what the caller can do) — see the [Threads guide](/api/threads/). ## Connections [Section titled “Connections”](#connections) A Connection (API name: *team link*) connects two Teams and gates all cross-Team activity. It carries an allowed data-flow **direction** — `send`, `receive`, or `send_receive`, expressed from the requesting Team’s perspective — and is established by a three-step handshake: the requester **creates** the link, the partner **accepts** it, and the requester **confirms**. Links are addressed by their invite `code`. | Operation | Method & path | | ------------------- | ------------------------------------------------------------------------------------------------ | | Create (invite) | `POST /api/v1/connections` — body `{ "allow": "send" \| "receive" \| "send_receive", "email"? }` | | List Connections | `GET /api/v1/connections` | | Get / delete one | `GET` / `DELETE /api/v1/connections/{code}` | | Accept (partner) | `PATCH /api/v1/connections/accept` | | Reject (partner) | `PATCH /api/v1/connections/reject` | | Confirm (requester) | `PATCH /api/v1/connections/confirm` | | Cancel | `PATCH /api/v1/connections/cancel` | | Pause / resume | `PATCH /api/v1/connections/pause` / `resume` | Pausing a Connection suspends the sharing and Shipment activity that depends on it without deleting the relationship. ### Direction amendments [Section titled “Direction amendments”](#direction-amendments) Changing an active Connection’s direction is itself a handshake, so neither side can unilaterally widen data flow — either Team proposes, the *other* Team accepts, and the proposer confirms; the old direction stays in force until confirmation: * `POST /api/v1/connections/amend/propose` — body `{ "code", "allow" }` * `PATCH /api/v1/connections/amend/accept` / `confirm` / `cancel` Shares whose flow the new direction no longer permits become dormant rather than being deleted. ## Shipments [Section titled “Shipments”](#shipments) A Shipment (API namespace: `/api/v1/transfers`, legacy naming) transfers ownership of Threads to a connected Team: assemble a draft manifest, send it, and the receiver responds. The endpoints, in lifecycle order: | Stage | Method & path | | ------------------------------------------ | ---------------------------------------------------------------------------------- | | Create draft | `POST /api/v1/transfers` | | Add / update / remove manifest items | `POST /api/v1/transfers/{transfer_id}/items`, `PATCH` / `DELETE …/items/{item_id}` | | Set primary Thread | `PUT /api/v1/transfers/{transfer_id}/primary-thread` | | Send | `POST /api/v1/transfers/{transfer_id}/send` | | Preview (receiver, after send) | `GET /api/v1/transfers/{transfer_id}/preview` | | Respond: accept / reject / request changes | `POST /api/v1/transfers/{transfer_id}/respond` | | Converse | `POST /api/v1/transfers/{transfer_id}/messages` | | Cancel (draft, sent, or change-requested) | `POST /api/v1/transfers/{transfer_id}/cancel` | | Retry a failed Shipment | `POST /api/v1/transfers/{transfer_id}/retry` | | Abandon a failed Shipment | `POST /api/v1/transfers/{transfer_id}/abandon` | | Restart from a stopped Shipment’s manifest | `POST /api/v1/transfers/{transfer_id}/start-from-prior-manifest` | | List (mail-box views) | `GET /api/v1/transfers?box=inbox\|outbox\|sent` | | Get one with its manifest | `GET /api/v1/transfers/{transfer_id}` | Responding takes `{ "value": "accept" | "reject" | "request_changes" }` (a `reason` is required for change requests). Listing supports `box`, `view`, `status`, and `direction=inbound|outbound` filters. Note This table is deliberately just a map. What a manifest contains, how disclosure works, and what each status means are covered in [Shipments](/use/shipments/); the provenance model behind cross-Team data visibility is [Fabric](/use/fabric/). ## Slices [Section titled “Slices”](#slices) A Slice derives a new Thread from an existing one within your own Team — a selected subset of fields, files, and identifiers — typically to prepare exactly what you intend to share or ship, keeping the rest private: * `POST /api/v1/slices` — slice one Thread (choose target Folder via `bundleId`, select `fields`, …) * `POST /api/v1/slices/batch` — derive many Threads in one operation * `GET /api/v1/slices/{slice_id}` — a Slice with its Fabric links ## Related pages [Section titled “Related pages”](#related-pages) * [Core model](/api/core-model/) — how Teams, shares, and Connections fit the domain * [Shipments](/use/shipments/) — Shipment lifecycle semantics * [Fabric](/use/fabric/) — cross-organization provenance and disclosure * [API reference](/reference/api/) — full schemas for every endpoint above # Threads API guide > Create, search, update, archive, and read Thread records and their field data. A **Thread** is the digital record for one physical item — an asset, part, document, or workflow item. It carries a name and description, typed field data, attached files, bound [identifiers](/api/identifiers/), and an event history. Threads are owned by a Team, so every request needs a bearer token plus the `Dust-Ctx-Org-Id` header (and `Dust-Ctx-Team-Id` to act as a specific Team) — see [Authentication](/api/authentication/) and [Conventions](/api/conventions/). This guide covers the main flows. For every parameter and response schema, see the [API reference](/reference/api/). ## Endpoints at a glance [Section titled “Endpoints at a glance”](#endpoints-at-a-glance) | Operation | Method & path | | --------------------------- | ------------------------------------------------------ | | Create one or many Threads | `POST /api/v1/threads` | | List / search Threads | `GET /api/v1/threads` | | Count Threads | `GET /api/v1/threads/count` | | Get one Thread | `GET /api/v1/threads/{thread_id}` | | Update metadata and fields | `POST /api/v1/threads/{thread_id}` | | Update fields only | `POST /api/v1/threads/{thread_id}/data` | | List archived field data | `GET /api/v1/threads/{thread_id}/data/archived` | | Restore archived field data | `POST /api/v1/threads/{thread_id}/data/restore` | | Archive Threads | `PATCH /api/v1/threads/archive` | | Restore Threads | `PATCH /api/v1/threads/restore` | | Check caller permissions | `POST /api/v1/threads/permissions` | | Presence heartbeat | `POST /api/v1/threads/{thread_id}/presence` | | List a Thread’s files | `GET /api/v1/threads/{thread_id}/files` | | Set / upload thumbnail | `PATCH` / `POST /api/v1/threads/{thread_id}/thumbnail` | ## Create a Thread [Section titled “Create a Thread”](#create-a-thread) `POST /api/v1/threads` accepts three body shapes, selected by `type`: `single` (one Thread), `list` (many normalized Threads), and `raw` (flat key-value records). All three accept an optional `bundleId` to create the Threads inside a [Folder](/api/core-model/). * curl ```bash curl -fsS "$APID_URL/api/v1/threads" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \ -H "Content-Type: application/json" \ -d '{ "type": "single", "thread": { "name": "Tire SZ3J-11-ZJ17" }, "data": [ { "name": "Serial Number", "type": "text", "value": { "text": "SZ3J-11-ZJ17" } }, { "name": "Max PSI", "type": "number", "value": { "number": 51 } } ] }' ``` * TypeScript ```ts const created = await client.threads.create({ type: "single", thread: { name: "Tire SZ3J-11-ZJ17" }, data: [ { name: "Serial Number", type: "text", value: { text: "SZ3J-11-ZJ17" } }, { name: "Max PSI", type: "number", value: { number: 51 } }, ], }); ``` Field values are nested under `value` by type — `{ "text": … }`, `{ "number": … }`, and so on. The spec defines inputs for text, long text, number, boolean, date, date range, date-time, time, duration, email, phone, URL, JSON, select, select-many, tags, resource (file) references, and thread references. ### Bulk import [Section titled “Bulk import”](#bulk-import) Use `type: "list"` when you already have normalized `{ thread, data }` objects, or `type: "raw"` to hand the API flat records — it derives fields from each object’s key-value pairs, using `nameKey` and `descriptionKey` (default `name` / `description`) for the Thread’s own metadata: ```json { "type": "raw", "nameKey": "serial", "raw": [ { "serial": "SZ3J-11-ZJ17", "part": "P355/30R19", "maxPsi": 51 } ] } ``` For importing whole assembly structures atomically, see `POST /api/v1/imports/plan` and `POST /api/v1/imports/commit` in the [reference](/reference/api/). ## Read Threads [Section titled “Read Threads”](#read-threads) `GET /api/v1/threads/{thread_id}` returns the Thread with its field data (an optional `maxEvents` query includes recent events). `GET /api/v1/threads` lists with cursor pagination (`cursor`, `pageSize`, `order`, `orderCol`) and supports filters including: | Filter | Meaning | | --------------------------------------- | --------------------------------------------------- | | `q`, `queryCol` | Text search, optionally restricted to one column | | `bundleId` | Threads in a Folder or Category | | `templateId` | Threads created from a Template | | `tagType` | Threads with an identifier of this type bound | | `hasResources` | Threads with attached files | | `includeArchived`, `archivedOnly` | Archive visibility | | `createdBy`, `ownedByTeam` | Provenance filters | | `excludeTransferred`, `transferredOnly` | Shipped-away Threads | | `withActiveShipment` | Annotate each item with its active Shipment, if any | `GET /api/v1/threads/count` takes the same filters and returns only the count — useful for dashboards and pagination summaries. ## Update a Thread [Section titled “Update a Thread”](#update-a-thread) Two endpoints, one intent split: * `POST /api/v1/threads/{thread_id}` — takes `{ thread, update?, remove? }`: Thread metadata (name, description, template, …) plus optional field changes in one call. * `POST /api/v1/threads/{thread_id}/data` — fields only: `{ threadId, update, remove?, expectedUpdatedAt? }`. Fields in `update` are upserted (matched by name/ID); `remove` takes field IDs. POST /api/v1/threads/{thread\_id}/data ```json { "threadId": "9f6a…", "update": [ { "name": "VIN", "type": "text", "value": { "text": "1HGCM82633A004352" } } ], "remove": [] } ``` Optimistic concurrency Pass `expectedUpdatedAt` (the `updatedAt` you last read) with data updates. If someone else changed the Thread in the meantime, the request fails instead of silently overwriting. ### Archived field data [Section titled “Archived field data”](#archived-field-data) Removing a field archives it rather than destroying it. `GET /api/v1/threads/{thread_id}/data/archived` lists archived fields, and `POST /api/v1/threads/{thread_id}/data/restore` brings them back by ID (`{ threadId, restore: ["field-id", …] }`). ## Archive and restore Threads [Section titled “Archive and restore Threads”](#archive-and-restore-threads) Archiving is bulk and reversible: * `PATCH /api/v1/threads/archive` — `{ threadIds: […], toggle? }`. With `toggle: true`, archived Threads in the list are unarchived and active ones archived in a single call. * `PATCH /api/v1/threads/restore` — restore archived Threads. Archived Threads disappear from default listings; use `includeArchived` or `archivedOnly` to see them. ## Permissions [Section titled “Permissions”](#permissions) Before rendering edit controls or attempting writes across many Threads, ask what the caller can actually do: ```bash curl -fsS "$APID_URL/api/v1/threads/permissions" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \ -H "Content-Type: application/json" \ -d '{ "threadIds": ["9f6a…", "c2d1…"] }' ``` `POST /api/v1/threads/permissions` returns the caller’s effective permissions per Thread in the current Team context. Related reads: `GET /api/v1/threads/{thread_id}/access` (which Teams provide access) and `GET /api/v1/threads/{thread_id}/shared` (who the Thread is shared with) — both covered in [Teams, sharing, and connections](/api/teams-and-sharing/). ## Presence [Section titled “Presence”](#presence) `POST /api/v1/threads/{thread_id}/presence` is a heartbeat: send it periodically while a user views a Thread (optionally with display `name` / `image`, and `leaving: true` on exit) and the response lists the Thread’s current viewers. DICE uses this for the “who else is here” indicator. ## Files and thumbnails [Section titled “Files and thumbnails”](#files-and-thumbnails) Files attach to Threads through the [Files API](/api/files/); the Thread-side reads live here: * `GET /api/v1/threads/{thread_id}/files` — the Thread’s files, cursor-paginated (`includeArchived` is required). * `GET /api/v1/threads/{thread_id}/files/{res_id}` / `POST …/files/{res_id}` — read and update a single attached file. * `POST /api/v1/threads/{thread_id}/thumbnail` — upload an image (multipart, `thumbnail` field) and set it as the Thread’s thumbnail in one step. * `PATCH /api/v1/threads/{thread_id}/thumbnail` — set the thumbnail from an existing resource ID or an image URI. ## Related pages [Section titled “Related pages”](#related-pages) * [Core model](/api/core-model/) — how Threads relate to everything else * [Identifiers API guide](/api/identifiers/) — binding physical identifiers to Threads * [Files API guide](/api/files/) — uploads and downloads * [API reference](/reference/api/) — full schemas for every endpoint above # TypeScript client (@dustid/apid-client) > The typed TypeScript client for the DUST API — setup, real calls, error handling, and the generate-your-own-types alternative. `@dustid/apid-client` is the typed TypeScript client for the DUST API. It is the same client the DICE web application uses in production, and its request/response types are generated directly from the API’s OpenAPI spec, so it stays in lockstep with the server. ## Availability [Section titled “Availability”](#availability) The package is currently distributed by DUST rather than published to the public npm registry — it ships with the platform and is available to integration customers on request (contact [](mailto:support@dustidentity.com)). If you’d rather not take a dependency, you can get the same type safety by [generating types from the OpenAPI spec](#alternative-generate-your-own-types) — that is exactly how this client’s own types are produced. The client has no runtime dependencies beyond a `fetch` implementation (built into Node 18+, Bun, Deno, and browsers; you can inject your own via the `fetcher` option). ## Construct a client [Section titled “Construct a client”](#construct-a-client) ```ts import { ApidClient } from "@dustid/apid-client"; const client = new ApidClient({ baseUrl: "https://apid.dustid.io", bearerToken: token, // Authorization: Bearer organizationId: orgId, // sent as Dust-Ctx-Org-Id teamId: teamId, // sent as Dust-Ctx-Team-Id }); ``` The full options type: ```ts type ApidClientOptions = { baseUrl: string; bearerToken?: string; organizationId?: string; // Dust-Ctx-Org-Id header teamId?: string; // Dust-Ctx-Team-Id header fetcher?: typeof globalThis.fetch; // custom fetch (proxies, testing) defaultHeaders?: HeadersInit | (() => HeadersInit); // e.g. Dust-Ctx-Locale logger?: Logger; // debug/error request logging }; ``` `organizationId` and `teamId` map onto the [context headers](/api/conventions/); when `teamId` is omitted the server defaults to the organization root team. Use `defaultHeaders` for anything extra you want on every request — for example localized error messages: ```ts const client = new ApidClient({ baseUrl, bearerToken, organizationId, defaultHeaders: { "Dust-Ctx-Locale": "zh-CN" }, }); ``` ### Switching context or token [Section titled “Switching context or token”](#switching-context-or-token) Clients are immutable; two helpers return a re-configured copy, which makes per-request or per-user scoping cheap: ```ts const asOtherTeam = client.withContext({ teamId: otherTeamId }); const asFreshToken = client.withToken(newBearerToken); ``` ## Resources and calls [Section titled “Resources and calls”](#resources-and-calls) The client groups endpoints into resources: `client.me`, `client.threads`, `client.bundles`, `client.files`, `client.tags` (Identifier operations — the `/api/v1/tags/*` endpoints), `client.teams`, `client.sharing`, `client.templates`, `client.events`, `client.relations`, `client.threadLinks`, `client.assemblies`, `client.transfers`, `client.slices`, `client.imports`, `client.fabric`, `client.users`, `client.certificates`, and `client.certificateForms`. Method names mirror the reference. Two real examples with threads: ```ts // GET /api/v1/threads — cursor-paginated list const page = await client.threads.list({ pageSize: 50, q: "tire" }); for (const thread of page.threads) { console.log(thread.threadId, thread.name); } if (page.next) { const nextPage = await client.threads.list({ pageSize: 50, cursor: page.next }); } ``` ```ts // POST /api/v1/threads — create, unwrapped to the single created record const created = await client.threads.createOne({ type: "single", thread: { name: "Tire SZ3J-11-ZJ17" }, data: [{ name: "Serial Number", type: "text", value: { text: "SZ3J-11-ZJ17" } }], }); // GET /api/v1/threads/{thread_id} const record = await client.threads.get(created.threadId); console.log(record.thread.name, record.events.length); ``` `threads.create` returns the raw batch response (`{ created, uploadResponses }`); `threads.createOne` is a convenience that returns `created[0]` and throws if the server created nothing. All request and response types are exported from the package root (`ThreadCreateRequest`, `ThreadQueryResponse`, `ThreadGetResponse`, …), along with the raw generated `paths` / `components` / `operations` types from the spec. ## Error handling [Section titled “Error handling”](#error-handling) Methods return the parsed JSON response on success and **throw `ApiError`** on any non-2xx status. `ApiError` carries the API’s standard [error body](/api/conventions/): ```ts import { ApiError } from "@dustid/apid-client"; try { await client.threads.get(threadId); } catch (error) { if (error instanceof ApiError) { // error.code stable error code, e.g. "NOT_FOUND", "UNAUTHORIZED" // error.status HTTP status number // error.message localized human-readable message // error.detail optional extra context (validation issues, etc.) // error.body the full { code, message, status, detail } payload if (error.code === "UNAUTHORIZED") { // token expired — re-exchange the API key and retry } } else { throw error; // network failure or non-JSON response } } ``` Two edge behaviors worth knowing: `204`/`205` responses resolve to `undefined`, and a response that isn’t valid JSON throws a plain `Error` (not `ApiError`). If you pass a `logger`, every failed request is logged with its `x-request-id` for support correlation. Server-side only The client sends your bearer token on every call. Run it in backend code — never construct it with long-lived credentials in a browser or mobile bundle. See [Authentication](/api/authentication/). ## Alternative: generate your own types [Section titled “Alternative: generate your own types”](#alternative-generate-your-own-types) The API serves its OpenAPI 3 spec at `https://apid.dustid.io/api/openapi.json`. [`openapi-typescript`](https://www.npmjs.com/package/openapi-typescript) turns it into a fully typed `paths`/`components` definition you can use with plain `fetch` or any spec-driven fetch wrapper: ```bash npx openapi-typescript@7 https://apid.dustid.io/api/openapi.json -o ``` ```ts import type { paths } from "./dust-api"; type ThreadList = paths["/api/v1/threads"]["get"]["responses"]["200"]["content"]["application/json"]; ``` Remember to set the `Authorization`, `Dust-Ctx-Org-Id`, and `Dust-Ctx-Team-Id` headers yourself — see [Request conventions](/api/conventions/). Regenerate the types whenever you pick up new API features; the spec at `/api/openapi.json` is always current for the server you fetched it from. ## See also [Section titled “See also”](#see-also) * [API quickstart](/api/quickstart/) — end-to-end flow using this client. * [Request conventions](/api/conventions/) — the headers and error contract the client implements. * [Full API reference](/reference/api/) — every endpoint and schema. # Building with AI agents > Machine-readable docs (llms.txt) and downloadable integration skills for AI coding agents working against the DUST platform. If you use an AI coding agent (Claude Code, Cursor, Copilot, or similar) to build against the DUST platform, this site ships two kinds of machine-readable material: **llms.txt indexes** of the documentation, and **integration skills** — self-contained instruction files your agent can load to integrate correctly on the first pass. Everything on this page is generated from the same sources as the docs themselves (the documentation content and the current OpenAPI spec) on every build, and carries the docs version it was generated from — so it cannot silently drift from the API. ## llms.txt [Section titled “llms.txt”](#llmstxt) Following the [llms.txt convention](https://llmstxt.org/), the site root serves: | File | Contents | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | | [`/llms.txt`](/llms.txt) | Site map: every page with a one-line description, plus pointers to the OpenAPI spec, the interactive reference, and the npm packages | | [`/llms-full.txt`](/llms-full.txt) | The full documentation content as a single plaintext document | | [`/llms-small.txt`](/llms-small.txt) | A minified variant for smaller context windows | Point your agent at `/llms.txt` to let it pick pages, or feed it `/llms-full.txt` when it needs the whole picture. ## The OpenAPI spec [Section titled “The OpenAPI spec”](#the-openapi-spec) The authoritative API surface is the OpenAPI 3 document: * Live from the API server: [`https://apid.dustid.io/api/openapi.json`](https://apid.dustid.io/api/openapi.json) * A build-time copy on this site: [`/openapi.json`](/openapi.json) * Interactive reference (Scalar): [`https://apid.dustid.io/api/docs`](https://apid.dustid.io/api/docs) ## Integration skills [Section titled “Integration skills”](#integration-skills) A skill is a single Markdown file in the SKILL.md format (YAML frontmatter with `name` and `description`, then instructions) that teaches an agent one integration end to end — auth, headers, the core flows, and the failure modes. The skills are versioned and self-contained: an agent that has only the skill file can complete the integration. [dice-api-integration](/skills/dice-api-integration/SKILL.md)Authenticate (API key → bearer), set context headers, and drive the core API flows: create threads, bind identifiers, upload files, share, ship.[Download](/skills/dice-api-integration/SKILL.md)Copy [dust-go-connect-integration](/skills/dust-go-connect-integration/SKILL.md)Make a web app scan DUST identifiers inside the DUST Go mobile app via @dustid/dust-go-connect.[Download](/skills/dust-go-connect-integration/SKILL.md)Copy The endpoint index inside `dice-api-integration` is generated from the OpenAPI spec at build time; both files record the docs and spec versions they were generated from. ### Installing a skill [Section titled “Installing a skill”](#installing-a-skill) 1. Download the skill file from the stable URL above (e.g. `/skills/dice-api-integration/SKILL.md`). 2. For **Claude Code**, place it at `.claude/skills/dice-api-integration/SKILL.md` in your project (the directory name matches the skill’s `name`). Claude discovers it automatically and loads it when the task matches. 3. For other agents, include the file in the agent’s context or system prompt — the file is plain Markdown and self-contained. Tip Re-download the skills when you update your integration: each file carries the docs version and OpenAPI spec version it was generated from, so you can tell at a glance which API era your copy describes. ## npm packages [Section titled “npm packages”](#npm-packages) * [`@dustid/dust-go-connect`](https://www.npmjs.com/package/@dustid/dust-go-connect) — the DUST Go scanning bridge for web apps (see [Integrate with DUST Go](/integrate/dust-go-connect/)). * `@dustid/apid-client` — the typed TypeScript API client (see [TypeScript client](/api/typescript-client/) for availability). # DUST Go > Mobile scanning workflows with DUST Go. DUST Go is the mobile entry point for DUST-enabled web experiences. It lets teams prototype and run scanning workflows without first building a custom native app. Embedded browser Loads DUST-compatible web pages with the camera and accessory permissions already wired up. On-device capture Captures DUST scans through supported optical accessories and returns the result to your web app. Web ↔ native handoff Bridges scan capture from native into your APID-backed web workflow with a small JS package. ## Typical flow [Section titled “Typical flow”](#typical-flow) 1. A user opens a DUST-enabled workflow in DUST Go. 2. The web app authenticates the user through AuthD. 3. The app asks DUST Go to capture a scan. 4. The scan is sent to the APID [Identifier endpoints](/api/identifiers/). 5. APID identifies or verifies the Identifier, or binds it to a Thread. For the end-to-end product workflow — what an Identifier is, when to identify versus verify, and how scans relate to Threads — see [Identifiers and Scanning](/use/identifiers-and-scanning/). ## Download [Section titled “Download”](#download) [![Download DUST Go on the App Store](https://tools.applemediaservices.com/api/badges/download-on-the-app-store/black/en-us?releaseDate=1727654400)](https://apps.apple.com/us/app/dust-go/id6636551540) ## Install the package [Section titled “Install the package”](#install-the-package) When integrating a web workflow with DUST Go, install the helper package: ```bash bun add @dustid/dust-go-connect ``` The package detects the DUST Go bridge from inside your page and exposes the scanner to your JavaScript. See [Integrate with DUST Go](/integrate/dust-go-connect/) for the full third-party integration walkthrough — detection, capture, and resolving scans against APID. ## Hardware requirements [Section titled “Hardware requirements”](#hardware-requirements) DUST capture needs a supported optical accessory in addition to the phone camera: * **Loupe** — a MagSafe accessory for supported iPhone models. Tip See [Supported Devices](/integrate/supported-devices/) for the iPhone models the Loupe accessory is validated against. ## Related [Section titled “Related”](#related) * [Integrate with DUST Go](/integrate/dust-go-connect/) — add scanning to your own web app. * [React Scanner](/integrate/react-scanner/) — a copy-ready component that resolves scans against APID. * [Authentication](/api/authentication/) — provisioning the APID credentials your workflow needs. # Integrate with DUST Go > Add DUST scanning to your own web app by running it inside the DUST Go mobile browser. DUST Go is an embedded mobile browser: it loads your web app in a WebView and exposes the device’s DUST scanning hardware to the page through a small JavaScript bridge, [`@dustid/dust-go-connect`](https://www.npmjs.com/package/@dustid/dust-go-connect). You build and host an ordinary web app; DUST Go supplies the camera, optical accessories, and capture pipeline — no native toolchain required. This tutorial walks through detecting DUST Go, capturing a scan, and resolving it against APID. Note A DUST scan hands your page a **raw capture (a base64-encoded JPEG) plus capture metadata — not a resolved Identifier**. Identification, verification, and binding all happen server-side through the [APID Identifier endpoints](/api/identifiers/), which require APID credentials. See [Authentication](/api/authentication/) for provisioning access. Building with an AI agent? An agent skill covering this integration ships with these docs — point your coding agent at [AI Agents](/integrate/ai-agents/) to scaffold the DUST Go bridge and APID calls for you. ## How it fits together [Section titled “How it fits together”](#how-it-fits-together) 1. A user opens your web app inside DUST Go (via an app link). 2. Your page imports `@dustid/dust-go-connect`; the library detects the DUST Go bridge and exposes a `connector`. 3. Your page calls `scanAsync()`. DUST Go opens the native scanner over your page. 4. On capture, DUST Go delivers a scan event back to your page: the payload carries the capture data and metadata. 5. Your app forwards the capture to APID (`/api/v1/tags/identify`, `/bind`, or `/verify`) to resolve it. ## Install [Section titled “Install”](#install) ```bash npm install @dustid/dust-go-connect ``` The package is dependency-free, MIT-licensed, and ships TypeScript types. ## Detect DUST Go [Section titled “Detect DUST Go”](#detect-dust-go) The `connector` export is `undefined` when your page is not running inside DUST Go, so the same build of your app can serve regular browsers and DUST Go: dust-go.ts ```ts import { connector } from "@dustid/dust-go-connect"; export const insideDustGo = Boolean(connector); ``` Two things to know: * **Import timing.** Detection happens at module-import time, in the browser. If you server-render, gate any connector usage on hydration. * **Server-side detection.** Recent DUST Go builds also tag the WebView User-Agent with `DustGo/ ()`, so your server can detect the app before any JavaScript runs. Treat this as a hint, not a security boundary — anyone can spoof a User-Agent. ## Capture a scan [Section titled “Capture a scan”](#capture-a-scan) The simplest path is the promise API — present the scanner and await one capture: scan.ts ```ts import { scanAsync } from "@dustid/dust-go-connect"; const payload = await scanAsync(); // payload: { type: 'DUST' | 'QR' | 'BARCODE' | 'DATA_MATRIX' | 'NFC', // data: string, metadata?: ScanMetadata } ``` `scanAsync()` returns a `Promise`. It rejects if the user closes the scanner without capturing, and it rejects immediately when called outside DUST Go (no connector present) — so guard on `connector` first if the same code path runs in regular browsers. For multi-scan workflows (e.g. scan-many-then-submit), use the listener API — the scanner stays open across captures: scan-many.ts ```ts import { connector } from "@dustid/dust-go-connect"; connector?.add("my-listener", (event) => { switch (event.type) { case "scan": handleScan(event.payload); break; case "hide": // scanner closed case "show": // scanner opened break; default: // Ignore unknown event types — the protocol may grow. break; } }); connector?.showScanner(); // later: connector?.hideScanner(); connector?.remove("my-listener"); ``` `addScanListener` is the same thing without the listener bookkeeping — it receives every scan and returns its own unsubscribe function: scan-many-simple.ts ```ts import { addScanListener } from "@dustid/dust-go-connect"; const stop = addScanListener((payload) => handleScan(payload)); // later: stop(); ``` One scanner session can deliver a series of captures, so **submit them one at a time**. A DUST payload is a large base64 JPEG; uploading several at once starves the connection and gives the operator no usable progress. Queue the payloads and await each submission before starting the next. ### What the scanner can do [Section titled “What the scanner can do”](#what-the-scanner-can-do) Hosts differ — a phone can adjust zoom and exposure, a USB microscope accessory exposes a different set, and an older build may expose nothing. Ask, rather than inferring from the User-Agent: capabilities.ts ```ts const capabilities = connector?.getCapabilities?.(); ``` Treat an absent or empty announcement as *single captures only, nothing adjustable*. Never treat silence as capability. ### What’s in the payload [Section titled “What’s in the payload”](#whats-in-the-payload) | `payload.type` | `payload.data` | Notes | | ------------------------------ | --------------------------------------- | -------------------------------------- | | `DUST` | Base64-encoded JPEG of the DUST capture | Large; resolve it server-side via APID | | `QR`, `BARCODE`, `DATA_MATRIX` | The decoded symbol contents | | | `NFC` | The hex id read from the NFC chip | | `payload.metadata` (typed `ScanMetadata`) describes the capture: device identifiers (`deviceId`, `modelName`, `osName`, `osVersion`, `appVersion`), lens/camera selection, and optionally optics fields (zoom, focus, exposure, ISO), geolocation (`latitude`/`longitude`/`accuracy`), and capture-source details for external scan accessories (`captureSource`, `usbVendorId`, `usbProductId`, `dragonBackend`). Forward it opaquely when binding — APID stores it with the Identifier. ## Resolve the scan against APID [Section titled “Resolve the scan against APID”](#resolve-the-scan-against-apid) The DUST capture is only useful once APID has matched it. Send it as multipart form data with your APID credentials: identify.ts ```ts async function identifyDustScan(base64Jpeg: string) { // The scan arrives base64-encoded; APID expects binary multipart data. const bytes = Uint8Array.from(atob(base64Jpeg), (c) => c.charCodeAt(0)); const form = new FormData(); form.set("tagType", "DUST"); form.set("data", new Blob([bytes], { type: "image/jpeg" })); // API field names keep legacy "group" naming — these are Team ids. form.set("searchGroupIds", JSON.stringify([TEAM_ID])); const response = await fetch(`${APID_URL}/api/v1/tags/identify`, { method: "POST", headers: { Authorization: `Bearer ${token}`, "Dust-Ctx-Org-Id": ORGANIZATION_ID, }, body: form, }); if (!response.ok) throw new Error(`identify failed: ${response.status}`); return await response.json(); } ``` `searchGroupIds` names the Teams whose Identifiers are searched. The same shape works for `/api/v1/tags/bind` (add `threadId`; DUST binds also need a client-generated `options.enrollmentSessionId` UUID) and `/api/v1/tags/verify` (add `threadId` and `tags`). See [Identifiers](/api/identifiers/) for the full request/response contracts, and the [React Scanner](/integrate/react-scanner/) for a copy-ready component that implements all three operations. Keep credentials server-side Do not embed long-lived APID tokens in page JavaScript. Have your backend either proxy the Identifier calls or mint short-lived, scoped tokens for the browser session. See [Authentication](/api/authentication/). ## Geolocation [Section titled “Geolocation”](#geolocation) Standard `navigator.geolocation` calls work inside DUST Go — the app transparently proxies them through the native OS permission prompt. No connector code needed. ## Sign-in flows inside DUST Go [Section titled “Sign-in flows inside DUST Go”](#sign-in-flows-inside-dust-go) If your app uses OAuth/OIDC, be aware that DUST Go hands external identity-provider navigations off to the system browser, and the callback returns to your page via a custom URL scheme. When you construct an OAuth `redirect_uri` in the page, always wrap it: ```ts import { connector } from "@dustid/dust-go-connect"; const origin = window.location.origin; const redirectUri = ( connector?.rewriteRedirect(new URL(`${origin}/auth/callback`)) ?? new URL(`${origin}/auth/callback`) ).href; ``` Outside DUST Go (and on hosts where no rewrite is needed) this is a no-op. Inside DUST Go it swaps the URL’s protocol for the app’s custom scheme, producing a URI like `com.dustidentity.dustgo://your-host/auth/callback` (the exact scheme comes from the DUST Go build; `dustgo` is the fallback), which your identity provider must have registered as an allowed redirect URI. Known limitation for third-party identity providers Current DUST Go builds automatically rewrite the `redirect_uri` query parameter on *any* cross-origin navigation that carries one, whether or not your provider knows DUST Go’s URL schemes. Providers that reject unregistered custom-scheme redirect URIs (most public IdPs) will fail to complete sign-in inside DUST Go. If your app needs third-party OAuth inside DUST Go, contact DUST Identity — and register the DUST Go schemes with your provider if it supports custom schemes. ## Test your integration [Section titled “Test your integration”](#test-your-integration) 1. Install DUST Go from the [App Store](https://apps.apple.com/us/app/dust-go/id6636551540) (see [Supported Devices](/integrate/supported-devices/) for hardware requirements — DUST capture needs a supported optical accessory). 2. Serve your app over HTTPS on a URL the device can reach (a LAN address works for development). 3. In DUST Go, add your URL as a custom app link and open it. 4. Verify the connector is detected, then run a scan. A minimal diagnostic page that exercises the whole bridge (detection, `scanAsync`, event log) is available in the package repository. ## Versioning and compatibility [Section titled “Versioning and compatibility”](#versioning-and-compatibility) * The connector speaks protocol version 2: at import time it sends an automatic `hello` handshake advertising which events your page supports. You don’t manage this — just keep the package reasonably current. * Event listeners receive every event type; switch on `event.type` and ignore types you don’t recognize (new ones may be added). * `calibrationResult` events and `ackCalibrationResults()` are internal plumbing for DUST’s first-party calibration workflows — third-party integrations can ignore them. # Record provenance from your ERP > Post Declared Transactions from an ERP, WMS, or MES as a Service Account, with each entry attributed to the human operator in your system. Business systems see events DICE never witnesses: a goods receipt in SAP, an inspection sign-off in your MES, a sale closed in your commerce platform. **Declared Transactions** let your integration write those moments onto a Thread’s history as they happen — each one an attributed, permanent entry that travels with the item’s provenance and can appear on its [Public Page](/use/public-pages/). This recipe wires an ERP (the same shape fits a WMS, MES, or any system of record) to `POST /api/v1/events/declare`, authenticating as a [Service Account](/api/authentication/) and attributing every entry to the human operator who acted in your system. Note A declared entry is your organization’s **attributed assertion — never a platform-verified fact**. DICE records who claimed it and when, displays it marked as declared, and never presents it as something the platform observed. It is also **permanent**: it is shared with downstream owners along with the rest of the Thread’s history and cannot be deleted, only retracted — and a retraction is a second visible entry, not an erasure. ## The flow at a glance [Section titled “The flow at a glance”](#the-flow-at-a-glance) 1. Your integration exchanges its Service Account credential for a short-lived bearer token ([Authentication](/api/authentication/)). 2. Something happens in your system — a goods issue, an inspection, a repair closure. 3. Your integration calls `POST /api/v1/events/declare` with the Thread id, a headline, and the claim’s time and place, sending the operator’s identity in the `Dust-Ctx-Declared-Actor` header. 4. The entry appears in the Thread’s Transaction History in DICE, marked **Declared**, attributed to the Service Account acting for your operator. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A **Service Account** with an API key or OAuth client — see [Authentication](/api/authentication/). The Service Account needs edit access to the Threads it will write to (grant it access to the owning Team). * Your organization id for the `Dust-Ctx-Org-Id` header, and the Team id if the Service Account should act as a specific Team — see [Request conventions](/api/conventions/). * The **Thread ids** of the items involved. An integration usually resolves these by searching on the field it shares with your system — a serial number, batch, or order number — via `GET /api/v1/threads` (see the [Threads API guide](/api/threads/)). ## Declare a transaction [Section titled “Declare a transaction”](#declare-a-transaction) One call records one entry on one Thread: * curl ```bash curl -fsS "https://apid.dustid.io/api/v1/events/declare" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \ -H 'Dust-Ctx-Declared-Actor: {"id": "JDOE", "system": "SAP", "displayName": "Jane Doe"}' \ -H "Content-Type: application/json" \ -d '{ "threadId": "0b9e7c9a-2f9d-4d8a-9a51-1c2e57ab8d10", "title": "Incoming inspection passed", "note": "Visual and dimensional inspection against PO 4500012345.", "kind": "inspection", "edtf": "2026-08-06", "location": { "name": "Plant 1710, Springfield" } }' ``` * TypeScript ```ts const response = await fetch("https://apid.dustid.io/api/v1/events/declare", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Dust-Ctx-Org-Id": orgId, "Dust-Ctx-Declared-Actor": JSON.stringify({ id: "JDOE", system: "SAP", displayName: "Jane Doe", }), "Content-Type": "application/json", }, body: JSON.stringify({ threadId: "0b9e7c9a-2f9d-4d8a-9a51-1c2e57ab8d10", title: "Incoming inspection passed", note: "Visual and dimensional inspection against PO 4500012345.", kind: "inspection", edtf: "2026-08-06", location: { name: "Plant 1710, Springfield" }, }), }); if (!response.ok) throw new Error(`declare failed: ${response.status}`); const claim = await response.json(); ``` Request fields — everything except `threadId` is optional, but an entirely empty declaration is rejected: | Field | Type | Notes | | ---------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `threadId` | UUID | The Thread the entry belongs to. Requires edit access. | | `title` | string ≤ 80 | Short headline — what feeds and pages show as the entry’s title. | | `note` | string ≤ 4000 | Free-text detail of what happened. | | `kind` | string ≤ 64 | Open-ended classification: `sale`, `inspection`, `repair`, `service`, … your vocabulary. Defaults to `other`. | | `edtf` | string ≤ 64 | When it happened, at the precision you actually know — see below. Omit to record it as of now. | | `location` | object | The asserted place: `{ "name": string, "latitude"?: number, "longitude"?: number }`. `name` is what renders. | | `resIds` | UUID\[] ≤ 25 | Evidence: ids of files **already attached to the Thread** that document the entry — an inspection report, a certificate. Ids of files not attached to that Thread are rejected. | The response returns the materialized claim — the `kind`, `title`, `note`, and a structured `when` object carrying the claim’s `display` string, precision, and bounds. ## State time at the precision you know [Section titled “State time at the precision you know”](#state-time-at-the-precision-you-know) `edtf` takes a subset of EDTF (ISO 8601-2), so the claim carries exactly the precision your system has — a year, a month, a day, a range, or an approximation: | Claim | `edtf` | Renders as | | -------------- | ------------ | ----------------- | | An exact day | `2026-07-14` | Jul 14, 2026 | | A month | `2026-07` | July 2026 | | A year | `1968` | 1968 | | A closed range | `1968/1970` | 1968–1970 | | Circa | `1835~` | Circa 1835 | | Before a date | `../1970-03` | Before March 1970 | | After a date | `2019/..` | After 2019 | The claim is displayed everywhere at the precision you stated — a `1968/1970` range is never collapsed to a fabricated exact date. Send the precision you actually have, not a midnight-timestamped guess. Tip ERP documents usually carry a posting date, not a timestamp. Post the date (`2026-08-06`), not a fabricated instant — the record is more honest and reads better on the Thread and its Public Page. ## Attribute the human operator [Section titled “Attribute the human operator”](#attribute-the-human-operator) A Service Account authenticates your *system*. The `Dust-Ctx-Declared-Actor` header names the *person* who acted in it, per request: ```http Dust-Ctx-Declared-Actor: {"id": "JDOE", "system": "SAP", "displayName": "Jane Doe", "role": "Quality Inspector"} ``` `id` is required; `system`, `displayName`, and `role` are optional; the JSON value must stay under 1 KB (URI-encode it if it contains non-ASCII characters). The declared actor is recorded verbatim on every entry the request writes and shown in history as *declared* attribution — supplied by your integration, not verified by DICE, and never affecting permissions. An organization admin can make it mandatory, in which case writes without it are rejected with `403 ATTRIBUTION_REQUIRED`. Full semantics: [Declared actor attribution](/api/authentication/#declared-actor-attribution). For declared provenance this header is worth treating as required in your own code: “Inspected — passed” is a far stronger claim with “Jane Doe, Quality Inspector” attached than with only “SAP Connector”. ## Declare across a whole lot [Section titled “Declare across a whole lot”](#declare-across-a-whole-lot) When one business event touches many items — a goods receipt of 200 serialized units, a lot-level inspection — declare once across all of them: ```bash curl -fsS "https://apid.dustid.io/api/v1/events/declare/batch" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \ -H 'Dust-Ctx-Declared-Actor: {"id": "JDOE", "system": "SAP"}' \ -H "Content-Type: application/json" \ -d '{ "threadIds": ["0b9e7c9a-…", "4f1d22c0-…", "9a8b11de-…"], "title": "Incoming inspection passed", "kind": "inspection", "edtf": "2026-08-06", "location": { "name": "Plant 1710, Springfield" } }' ``` * `threadIds` takes 1–500 Thread ids; the write is **all-or-nothing** and requires edit access to every Thread in the batch. * The same claim lands on every Thread — there is no per-Thread variation in a batch. Data that differs per item (serial, batch, measurement results) belongs in Thread fields, not in the claim. * The response includes a shared **`operationId`**. Store it: it is the batch’s correction handle (below). Tip The batch endpoint also works with a single Thread id, and unlike the single-entry endpoint it returns an `operationId`. If your integration may ever need to correct an entry programmatically, declaring through the batch endpoint and storing the `operationId` alongside your source document gives you a durable handle. ## Backfill many entries at once [Section titled “Backfill many entries at once”](#backfill-many-entries-at-once) `/declare/batch` writes **one** claim to many Threads. When you have *many different* claims to post — a historical backfill, a migration off a spreadsheet system, a day’s worth of shop-floor events — use `/declare/rows` instead. Every row is written to every Thread in `threadIds`, and the whole thing shares one `operationId`: ```bash curl -fsS "https://apid.dustid.io/api/v1/events/declare/rows" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \ -H 'Dust-Ctx-Declared-Actor: {"id": "JDOE", "system": "SAP"}' \ -H "Content-Type: application/json" \ -d '{ "threadIds": ["0b9e7c9a-…"], "rows": [ { "title": "Inspected", "kind": "inspection", "edtf": "2024-03-01", "location": { "name": "Geneva" } }, { "title": "Sealed for shipment", "kind": "shipment", "edtf": "2024-03-04" }, { "title": "Customs cleared", "edtf": "2024-03-11" } ] }' ``` * Up to 100 rows and 500 Threads, capped at **2,000 total entries** (`threadIds.length × rows.length`) per request. Split larger backfills. * **All-or-nothing across the whole request**: one unrecognized date rejects every row, rather than leaving a partial backfill of entries that can only be retracted one by one. * Rows carry no `anchor` and no evidence `resIds` — both are single-claim gestures. Use `/declare` for those. * `/declare/batch` is the one-row case of this endpoint; keep using it when the claim really is one claim. This is the same endpoint DICE’s own CSV import posts to. If your customers are backfilling by hand rather than from a system, point them at [Recording past events](/use/recording-past-events/) instead of building an integration. ## Correct a mistake [Section titled “Correct a mistake”](#correct-a-mistake) Declared entries are immutable — there is no edit and no delete. The correction is a **retraction**: a second attributed entry stating the first was wrong. The original stays in the history marked retracted, and both travel downstream with the record — errata, never erasure. To retract everything one batch wrote (say the goods receipt was reversed in your ERP), post the stored `operationId`: ```bash curl -fsS "https://apid.dustid.io/api/v1/events/retract/by-operation" \ -H "Authorization: Bearer $DUST_TOKEN" \ -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \ -H "Content-Type: application/json" \ -d '{ "operationId": "7c3f0f9e-5b7a-4a4f-8f7d-2f1d0e6a9b21", "reason": "Goods receipt reversed (movement type 102)." }' ``` This retracts every still-live entry the operation wrote — entries already retracted individually are skipped — and requires edit access to every Thread involved. A single entry is retracted by its event id instead: `POST /api/v1/events/{event_id}/retract` with an optional `reason`. Event ids come from the Thread’s history (`GET /api/v1/events?threadId=…`). After retracting, record a corrected entry with a fresh declare — that pair, wrong entry plus correction, is the honest shape of the record. Caution A Public Page publishes a snapshot, so a new entry or a retraction reaches the page only when the page republishes. When the page’s design uses the living passport policy, a successful declare or retract republishes it automatically against the version it already serves. Otherwise the page keeps showing the previous snapshot, is listed as behind its record, and updates on the next publish. Either way the withdrawn claim renders struck through and marked withdrawn rather than disappearing — published provenance is never silently rewritten. ## Failure modes [Section titled “Failure modes”](#failure-modes) | Response | Meaning | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400 INVALID_DATA` | The `edtf` value is outside the supported subset or not a real calendar date, the declaration is empty, or an evidence id is not attached to that Thread. | | `400 INVALID_REQUEST` | Malformed body — e.g. a field over its length bound. | | `403 ATTRIBUTION_REQUIRED` | The Service Account’s policy requires a declared actor and the request carried none. | | `404 NOT_FOUND` | A Thread id the caller cannot see or that does not exist. On the batch endpoint, any one such id fails the whole batch. | Error bodies follow the standard contract — see [Request conventions](/api/conventions/). ## Next steps [Section titled “Next steps”](#next-steps) * [Authentication and API keys](/api/authentication/) — Service Accounts, token exchange, declared actor semantics. * [Threads API guide](/api/threads/) — resolving your system’s serials and orders to Thread ids. * [Recording past events](/use/recording-past-events/) — the same feature as your operators see it in DICE. * [Public Pages](/use/public-pages/) — how declared entries appear on the item’s public passport. # React Scanner > A copy-ready React scanner for DUST, QR, barcode, Data Matrix, and NFC. A small React component that calls APID directly. It supports three modes: * **DUST** — uploads a DUST scan image to the APID [Identifier endpoints](/api/identifiers/). * **Other** — uses the device camera to read QR, barcode, and Data Matrix Identifiers. * **Manual** — submits QR, barcode, Data Matrix, or NFC Identifiers from a text field. Mint credentials server-side — never ship a long-lived token The component takes a `bearerToken` prop, which means the token is visible to anyone using the page. Never embed a long-lived APID token in client code or a build-time environment variable. Instead, have your own backend mint a **short-lived, narrowly scoped token** per browser session (or proxy the Identifier calls entirely) and pass that to the component. See [Authentication](/api/authentication/) for token exchange and scoping. ## Install [Section titled “Install”](#install) * Existing React app ```bash bun add html5-qrcode ``` The component expects React to already be present in your app. * New React app ```bash bun add react react-dom html5-qrcode ``` ## Use it [Section titled “Use it”](#use-it) Fetch a short-lived token from your own backend, then render the scanner: IdentifyPage.tsx ```tsx import { useEffect, useState } from "react"; import { DustScanner } from "./DustScanner"; import "./scanner.css"; export function IdentifyPage() { const [token, setToken] = useState(null); useEffect(() => { // Your endpoint: exchanges the user's session for a short-lived APID token. fetch("/api/scanner-token") .then((res) => res.json()) .then(({ token }) => setToken(token)); }, []); if (!token) return

Preparing scanner…

; return ( console.log("scan result", result)} /> ); } ``` `searchGroupIds` names the Teams whose Identifiers are searched during identify — the prop keeps the API’s legacy `group` field naming. The optional `groupId` prop pins the request’s Team context (sent as the `Dust-Ctx-Team-Id` header). ## Bind to a Thread [Section titled “Bind to a Thread”](#bind-to-a-thread) ```tsx ``` ## Verify an Identifier [Section titled “Verify an Identifier”](#verify-an-identifier) ```tsx ``` See [Identifiers](/api/identifiers/) for what each operation returns, and [Integrate with DUST Go](/integrate/dust-go-connect/) if your app runs inside the DUST Go mobile browser instead of using the device camera directly. ## Component source [Section titled “Component source”](#component-source) **DustScanner.tsx** — click to expand DustScanner.tsx ```tsx import { Html5Qrcode, type Html5QrcodeResult } from "html5-qrcode"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; type ScannerMode = "dust" | "other" | "manual"; type ScanOperation = "identify" | "bind" | "verify"; type NonDustTagType = "QR" | "BAR_CODE" | "DATA_MATRIX" | "NFC"; type TagType = "DUST" | NonDustTagType; type VerifyTag = { tagId: string; tagType: TagType; }; type DustScannerProps = { apidUrl?: string; bearerToken: string; organizationId: string; groupId?: string; operation: ScanOperation; threadId?: string; verifyTags?: VerifyTag[]; searchGroupIds?: string[]; tagDescription?: string; onResult?: (result: unknown) => void; onError?: (error: Error) => void; }; type Detection = { tagType: NonDustTagType; value: string; format?: string; }; const FORMAT_TO_TAG: Record = { QR_CODE: { tagType: "QR", format: "qrcode" }, DATA_MATRIX: { tagType: "DATA_MATRIX", format: "datamatrix" }, AZTEC: { tagType: "DATA_MATRIX", format: "azteccode" }, MAXICODE: { tagType: "DATA_MATRIX", format: "maxicode" }, EAN_13: { tagType: "BAR_CODE", format: "ean13" }, EAN_8: { tagType: "BAR_CODE", format: "ean8" }, UPC_A: { tagType: "BAR_CODE", format: "upca" }, UPC_E: { tagType: "BAR_CODE", format: "upce" }, CODE_39: { tagType: "BAR_CODE", format: "code39" }, CODE_93: { tagType: "BAR_CODE", format: "code93" }, CODE_128: { tagType: "BAR_CODE", format: "code128" }, ITF: { tagType: "BAR_CODE", format: "interleaved2of5" }, CODABAR: { tagType: "BAR_CODE", format: "rationalizedCodabar" }, PDF_417: { tagType: "BAR_CODE", format: "pdf417" }, }; // Manual entry can't know the concrete symbology, so choose a canonical // format from the same vocabulary the camera path uses. const MANUAL_FORMATS: Record = { QR: FORMAT_TO_TAG.QR_CODE.format, BAR_CODE: FORMAT_TO_TAG.CODE_128.format, DATA_MATRIX: FORMAT_TO_TAG.DATA_MATRIX.format, NFC: "nfc", }; function normalizeIdentifier(value: string) { const trimmed = value.trim(); if (!trimmed || trimmed.length > 2000) return null; try { const parsed = JSON.parse(trimmed); if (parsed && typeof parsed === "object" && typeof parsed.id === "string") { return parsed.id.trim() || null; } } catch { // Plain text identifiers are valid. } return trimmed; } function detectNonDust(decodedText: string, result: Html5QrcodeResult): Detection | null { const value = normalizeIdentifier(decodedText); const formatName = result.result.format?.formatName?.toUpperCase(); if (!value || !formatName) return null; const mapped = FORMAT_TO_TAG[formatName]; if (!mapped) return null; return { tagType: mapped.tagType, value, format: mapped.format, }; } function appendJson(form: FormData, key: string, value: unknown) { if (value === undefined) return; form.set(key, typeof value === "string" ? value : JSON.stringify(value)); } export function DustScanner({ apidUrl = "https://apid.dustid.io", bearerToken, organizationId, groupId, operation, threadId, verifyTags = [], searchGroupIds, tagDescription, onResult, onError, }: DustScannerProps) { const [mode, setMode] = useState("dust"); const [manualType, setManualType] = useState("QR"); const [manualValue, setManualValue] = useState(""); const [busy, setBusy] = useState(false); const [message, setMessage] = useState(null); const qrRegionId = useMemo(() => `dust-non-dust-scanner-${crypto.randomUUID()}`, []); const qrRef = useRef(null); const lastDetectionRef = useRef(null); const submitForm = useCallback( async (path: string, form: FormData) => { const headers = new Headers({ Authorization: `Bearer ${bearerToken}`, "Dust-Ctx-Org-Id": organizationId, }); if (groupId) headers.set("Dust-Ctx-Team-Id", groupId); const response = await fetch(`${apidUrl.replace(/\/+$/, "")}${path}`, { method: "POST", headers, body: form, }); const text = await response.text(); const data = text ? JSON.parse(text) : null; if (!response.ok) { throw new Error(data?.message ?? `APID request failed with ${response.status}`); } return data; }, [apidUrl, bearerToken, groupId, organizationId], ); const runScan = useCallback( async (scan: { tagType: TagType; data: string | Blob; metadata?: Record }) => { setBusy(true); setMessage(null); try { const form = new FormData(); form.set("tagType", scan.tagType); form.set("data", scan.data); if (operation === "identify") { appendJson(form, "searchGroupIds", searchGroupIds); const result = await submitForm("/api/v1/tags/identify", form); onResult?.(result); setMessage("Identify complete."); return; } if (!threadId) { throw new Error("threadId is required for bind and verify operations."); } form.set("threadId", threadId); if (operation === "bind") { if (tagDescription) form.set("tagDescription", tagDescription); if (scan.tagType === "DUST") { appendJson(form, "options", { enrollmentSessionId: crypto.randomUUID() }); } else if (scan.metadata) { appendJson(form, "options", { metadata: scan.metadata }); } const result = await submitForm("/api/v1/tags/bind", form); onResult?.(result); setMessage("Bind complete."); return; } if (verifyTags.length === 0) { throw new Error("verifyTags is required for verify operations."); } appendJson(form, "tags", verifyTags); const result = await submitForm("/api/v1/tags/verify", form); onResult?.(result); setMessage("Verify complete."); } catch (error) { const err = error instanceof Error ? error : new Error("Unknown scanner error"); setMessage(err.message); onError?.(err); } finally { setBusy(false); } }, [onError, onResult, operation, searchGroupIds, submitForm, tagDescription, threadId, verifyTags], ); const runScanRef = useRef(runScan); runScanRef.current = runScan; const onErrorRef = useRef(onError); onErrorRef.current = onError; const handleDustFile = useCallback( async (file: File | null) => { if (!file) return; await runScan({ tagType: "DUST", data: file }); }, [runScan], ); const handleManualSubmit = useCallback(async () => { const value = normalizeIdentifier(manualValue); if (!value) { setMessage("Enter an identifier value."); return; } await runScan({ tagType: manualType, data: value, metadata: { format: MANUAL_FORMATS[manualType] }, }); setManualValue(""); }, [manualType, manualValue, runScan]); useEffect(() => { if (mode !== "other") return; let cancelled = false; const scanner = new Html5Qrcode(qrRegionId, false); qrRef.current = scanner; // Keep the start promise so cleanup can wait for an in-flight startup // before stopping — otherwise a fast unmount leaves the camera running. const startPromise = scanner .start( { facingMode: "environment" }, { fps: 8, qrbox: { width: 260, height: 260 } }, async (decodedText, result) => { const detection = detectNonDust(decodedText, result); if (!detection) return; const key = `${detection.tagType}:${detection.format}:${detection.value}`; if (lastDetectionRef.current === key) return; lastDetectionRef.current = key; await runScanRef.current({ tagType: detection.tagType, data: detection.value, metadata: detection.format ? { format: detection.format } : undefined, }); window.setTimeout(() => { if (lastDetectionRef.current === key) lastDetectionRef.current = null; }, 2000); }, () => {}, ) .catch((error) => { if (!cancelled) { const err = error instanceof Error ? error : new Error("Could not start camera scanner."); setMessage(err.message); onErrorRef.current?.(err); } }); return () => { cancelled = true; qrRef.current = null; void startPromise.then(async () => { if (scanner.isScanning) await scanner.stop().catch(() => undefined); scanner.clear(); }); }; }, [mode, qrRegionId]); return (
{(["dust", "other", "manual"] as const).map((nextMode) => ( ))}
{mode === "dust" ? ( ) : null} {mode === "other" ?
: null} {mode === "manual" ? (
setManualValue(event.currentTarget.value)} onKeyDown={(event) => { if (event.key === "Enter") void handleManualSubmit(); }} />
) : null} {message ?

{message}

: null} {busy ?

Processing scan...

: null}
); } ``` **scanner.css** — click to expand scanner.css ```css .dust-scanner { display: grid; gap: 1rem; max-width: 42rem; } .dust-scanner__modes { display: inline-flex; width: fit-content; gap: 0.25rem; border: 1px solid #d4d4d8; border-radius: 8px; padding: 0.25rem; } .dust-scanner__modes button { border: 0; border-radius: 6px; background: transparent; padding: 0.45rem 0.75rem; cursor: pointer; } .dust-scanner__modes button[aria-pressed="true"] { background: #111827; color: white; } .dust-scanner__dropzone, .dust-scanner__camera, .dust-scanner__manual { border: 1px solid #d4d4d8; border-radius: 8px; padding: 1rem; } .dust-scanner__dropzone { display: grid; gap: 0.75rem; } .dust-scanner__camera { min-height: 320px; } .dust-scanner__manual { display: flex; flex-wrap: wrap; gap: 0.5rem; } .dust-scanner__manual input { min-width: min(100%, 18rem); flex: 1; } ``` # Supported Devices > iPhone compatibility for DUST Go scan accessories. Depending on your mobile device, DUST offers scanner accessories that have been evaluated for compatibility with the [DUST Go](/integrate/dust-go/) mobile app. DUST capture always requires one of these optical accessories in addition to the phone camera; QR, barcode, Data Matrix, and NFC scanning work with the device hardware alone. ## Supported iPhone models [Section titled “Supported iPhone models”](#supported-iphone-models) The Loupe accessory is compatible with thin MagSafe cases on the following iPhone models. | | Standard | Mini / Air / e | Plus | Pro | Pro Max | | --------- | -------- | -------------- | ---- | --- | ------- | | iPhone 13 | ✓ | ✕ | N/A | ✓ | ✓ | | iPhone 14 | ✓ | N/A | ✓ | ✓ | ✓ | | iPhone 15 | ✓ | N/A | ✓ | ✓ | ✓ | | iPhone 16 | ✓ | N/A | ✓ | ✓ | ✓ | | iPhone 17 | ✓ | ✕ | N/A | ✓ | ✓ | ✓ Supported with the DUST Identity Loupe accessory · ✕ Not supported · N/A — model not produced ## Baseline expectations [Section titled “Baseline expectations”](#baseline-expectations) * Current iPhone hardware supported by the DUST Go release you are deploying. * A supported DUST optical accessory for scan capture (the Loupe). * Network access to AuthD and APID (see [Environments](/reference/environments/) for hostnames). * Camera permissions enabled for the app. ## Other scan types [Section titled “Other scan types”](#other-scan-types) QR, barcode, Data Matrix, and NFC scanning in DUST Go use the phone’s built-in camera and NFC reader — no DUST accessory is required for those Identifier types. The accessory matrix above applies only to DUST capture. ## Validation [Section titled “Validation”](#validation) Validate the full workflow on the exact device, accessory, lighting, and object surfaces used in production. DUST scan quality depends on the physical setup, not only the API integration. When testing a web integration, serve your app over HTTPS on a URL the device can reach (a LAN address works for development) and add it as a custom app link in DUST Go — see [Test your integration](/integrate/dust-go-connect/#test-your-integration). ## Related [Section titled “Related”](#related) * [DUST Go](/integrate/dust-go/) — the mobile app these accessories pair with. * [Integrate with DUST Go](/integrate/dust-go-connect/) — add scanning to your own web app. * [Identifiers and Scanning](/use/identifiers-and-scanning/) — the scanning workflows these devices power. # API Reference > The full generated APID reference, live and as a raw OpenAPI spec. The authoritative API reference is generated by APID and served live. Every endpoint, schema, and error shape documented there comes straight from the running server. [Interactive reference](https://apid.dustid.io/api/docs)Scalar API reference, served live by APID. [OpenAPI JSON (live)](https://apid.dustid.io/api/openapi.json)Raw OpenAPI 3 spec served by APID — pipe it into your tool of choice. [OpenAPI JSON (this site)](/openapi.json)Build-time copy served at /openapi.json — matches the docs you are reading. ## Spec copies [Section titled “Spec copies”](#spec-copies) Two copies of the spec are available: | Source | URL | Freshness | | --------------- | ----------------------------------------- | ------------------------------------------------------------------- | | DUST API (live) | `https://apid.dustid.io/api/openapi.json` | Always matches the deployed API. | | This docs site | `/openapi.json` | Generated when this site is built, from the same source as the API. | Prefer the live copy when generating clients or validating requests — it always matches the deployed API. The site copy is regenerated with every docs release and is convenient when you are already browsing here. Tip The [TypeScript client](/api/typescript-client/) is generated from this same spec, so its types always match what the reference documents. ## Where to start [Section titled “Where to start”](#where-to-start) * [Quickstart](/api/quickstart/) — first authenticated request in minutes. * [Authentication](/api/authentication/) — API keys, token exchange, and context headers. * [Conventions](/api/conventions/) — pagination, errors, and request context shared by every endpoint. ## Embedded reference [Section titled “Embedded reference”](#embedded-reference) [DUST API reference](https://apid.dustid.io/api/docs) # Environments > DUST service URLs, API paths, and health endpoints. DUST runs as three deployed services. Most integrations only need the DUST API; AuthD is reached through the API’s `/api/auth/*` paths. | Service | URL | Purpose | | ------------ | ------------------------- | ---------------------------------------------------------- | | DICE web app | `https://dice4.dustid.io` | First-party web UI for DUST workflows. | | DUST API | `https://apid.dustid.io` | Core API. Scalar reference at `/api/docs`. | | AuthD | `https://authd.dustid.io` | Accounts, Organizations, sessions, OIDC, Service Accounts. | ## API paths [Section titled “API paths”](#api-paths) The DUST API exposes the platform under these roots: | Path | Use | | ------------------- | --------------------------------------------- | | `/api/auth/token` | Exchange an AuthD API key for a bearer token. | | `/api/auth/jwks` | Fetch AuthD JWKS through the DUST API. | | `/api/v1/*` | Core DUST API operations. | | `/api/openapi.json` | Generated OpenAPI spec served by the API. | | `/api/docs` | Interactive Scalar reference. | See the [API Reference](/reference/api/) for the full generated endpoint catalog, and [Conventions](/api/conventions/) for the request context headers every call carries. ## Health endpoints [Section titled “Health endpoints”](#health-endpoints) The DUST API serves standard health probes at the server root (not under `/api`): | Path | Check | Failure behavior | | ---------- | ------------------------------------------- | ------------------------------------------------------------------ | | `/livez` | Liveness — the process is up. | Always `200` while the server runs. | | `/readyz` | Readiness — includes a database round-trip. | `503` if the database check fails or exceeds its 2-second timeout. | | `/healthz` | Alias for the readiness check. | Same as `/readyz`. | All three return `{ "status": "ok", "timestamp": "…" }` when healthy; the readiness checks return `{ "status": "error", … }` with a `503` when not. Point uptime monitors at `/readyz` (or `/healthz`). # Glossary > Definitions of the user-facing DUST platform vocabulary. The DUST platform vocabulary, in alphabetical order. Docs prose uses these terms consistently; API endpoint paths and field names sometimes keep older implementation names (noted per term). ### Assembly [Section titled “Assembly”](#assembly) A [Thread](#thread) that aggregates other Threads as its attached parts, while remaining a fully first-class Thread itself — with its own name, Fields, Identifiers, and Certificate. An Assembly can be nested inside another Assembly. Every Thread reachable inside an Assembly belongs to the same owning Team. ### Bind [Section titled “Bind”](#bind) The act of associating an [Identifier](#identifier) with a [Thread](#thread), performed through the [Identifier endpoints](/api/identifiers/). Once bound, scanning the Identifier resolves back to that Thread. ### Category [Section titled “Category”](#category) A cross-cutting grouping that classifies [Threads](#thread). Unlike a [Folder](#folder), a Thread can belong to many Categories at once. Categories can nest under a parent Category (subcategories); membership is direct, not rolled up. ### Certificate [Section titled “Certificate”](#certificate) An immutable, point-in-time PDF generated from a [Thread](#thread)’s data and issued by the platform. It is stored as a [Resource](#resource) attached to the Thread, with a record carrying its trust status (issued or voided) and provenance. The Resource checksum is the certificate hash. ### Certificate Form [Section titled “Certificate Form”](#certificate-form) A reusable, Team-owned design that defines how a [Certificate](#certificate) is laid out and which [Thread](#thread) data fills it. A Certificate Form locates values by field name (with optional aliases), not by [Template](#template), so it can generate Certificates for owned, shared, and transferred Threads alike. ### Connection [Section titled “Connection”](#connection) The standing relationship between two [Teams](#team) in different [Organizations](#organization) — the channel for all cross-team exchange. Sharing and [Shipments](#shipment) are only initiated over a Connection, and its lifecycle (pending, connected, paused, deleted) and direction (send, receive, or both) govern what may flow between the pair. Teams within one Organization collaborate without a Connection. ### Design Version [Section titled “Design Version”](#design-version) An immutable, numbered version of a [Public Page Design](#public-page-design) — v1, v2, v3 — frozen when the design is published, and the only thing “version” names in the Public Pages module (a page’s own publications are identified by date, never numbered). Publishing a version does not change any live page; pages move onto it only through a rollout, which is forward-only: a Design Version is never restored, and a bad one is corrected by publishing a newer one. ### Disclosure [Section titled “Disclosure”](#disclosure) The explicit act by which a [Thread](#thread) owner makes selected data — Fields, Resources, Identifiers, or Certificates — visible downstream through [Fabric](#fabric). Transferred copies are snapshots: source edits propagate only when the owner explicitly discloses them, and a made disclosure is visible to every downstream consumer on the chain. ### DUST (identifier) [Section titled “DUST (identifier)”](#dust-identifier) The physical DUST mark applied to an object, scanned as an image capture and resolved server-side by identifying or verifying it against enrolled Identifiers. A DUST [Identifier](#identifier) represents a physical identity and is never copied between Threads. See [Identifiers and Scanning](/use/identifiers-and-scanning/). ### Fabric [Section titled “Fabric”](#fabric) The cross-team provenance graph of the platform. When a [Thread](#thread) is transferred or sliced, Fabric records the resulting links between source and derived Threads, and carries [Disclosures](#disclosure) along them — independent of live sharing or [Connection](#connection) state. ### Field [Section titled “Field”](#field) A named, typed value on a [Thread](#thread) — text, numbers, dates, images, and other attributes that describe the item the Thread represents. [Templates](#template) define reusable sets of Fields. ### Folder [Section titled “Folder”](#folder) A nestable container that organizes [Threads](#thread) and other Folders. A Thread lives in at most one Folder (single-home — contrast with [Category](#category)). Sharing a Folder cascades access to its contents. ### Identifier [Section titled “Identifier”](#identifier) A scannable physical or digital code associated with a [Thread](#thread): DUST, QR, barcode, Data Matrix, or NFC. The user-facing term for what the API calls a tag — endpoint paths and field names keep the legacy naming (`/api/v1/tags`, `tagType`). See [Identifiers](/api/identifiers/) and [Identifiers and Scanning](/use/identifiers-and-scanning/). ### Identify [Section titled “Identify”](#identify) The act of finding a [Thread](#thread) by scanning or entering an [Identifier](#identifier). The Teams searched are set per request (the API’s `searchGroupIds`). See [Identifiers](/api/identifiers/). ### Manifest [Section titled “Manifest”](#manifest) The draft contents of a [Shipment](#shipment) or other [Fabric](#fabric) operation: the selected [Threads](#thread), their selected assets, and any Assembly, Folder, or Category structure to carry along. The manifest freezes when the Shipment is sent. ### Organization [Section titled “Organization”](#organization) The top-level account boundary, containing [Teams](#team) and their members. API requests carry the active Organization in the `Dust-Ctx-Org-Id` header. See [Conventions](/api/conventions/). ### Part / Position [Section titled “Part / Position”](#part--position) A **Part** is a [Thread](#thread) attached to an [Assembly](#assembly) — each Thread is a part of at most one Assembly, and by default inherits the Assembly’s access. A **Position** is a named Thread-valued Field on an Assembly assigned to one of its attached Threads (for example “front wheel”). ### Public Page [Section titled “Public Page”](#public-page) The public, unauthenticated web view of a [Thread](#thread) — the digital product passport a consumer reaches by scanning an [Identifier](#identifier) or following a printed link. Its URL is permanent and can be reserved (and its QR code printed) before anything is published; until then it serves a “registered” notice. A Public Page carries no content configuration of its own: it shows exactly what its [Public Page Design](#public-page-design) pulls, pinning one [Design Version](#design-version) and one publication of its data. See [Public Pages](/use/public-pages/). ### Public Page Design [Section titled “Public Page Design”](#public-page-design) A reusable, [Team](#team)-owned design that determines the content and appearance of every [Public Page](#public-page) published through it — an ordered stack of blocks that pull [Thread](#thread) data by field name, in the same way a [Certificate Form](#certificate-form) does. One design serves a whole product line; editing it changes nothing public until a [Design Version](#design-version) is published and rolled out. ### Publish Wave [Section titled “Publish Wave”](#publish-wave) A scoped background run that publishes or republishes every [Public Page](#public-page) in a [Folder](#folder), [Category](#category), [Template](#template), or explicit selection through one [Design Version](#design-version), with per-Thread success and failure accounting. A wave can be retried for its failures, or cancelled — which stops the remaining pages without moving already-published ones back. ### Publisher [Section titled “Publisher”](#publisher) A grant on a [Team](#team) membership meaning “may make this Team’s data public”. It gates every change in the Public Pages module — authoring and publishing [Public Page Designs](#public-page-design), publishing and unpublishing pages, rollouts, and [Publish Waves](#publish-wave). Team admins always hold it; members without it have read-only access to the module. Service Accounts can hold it, for pipeline publishing. ### Relationship [Section titled “Relationship”](#relationship) A directional link type used to connect two [Threads](#thread) owned by the same [Team](#team) (for example “supplied by”). Relationship links are free-form annotations; unlike [Part](#part--position) attachment they do not grant access and do not nest. ### Resource [Section titled “Resource”](#resource) A file attached to a [Thread](#thread) — documents, images, and other uploads. Resources can be selected into [Manifests](#manifest), [Slices](#slice), and [Disclosures](#disclosure). ### Shipment [Section titled “Shipment”](#shipment) The user-facing name in DICE for a [Transfer](#transfer): an outbound or inbound package of Threads exchanged between connected [Teams](#team), moving through draft, sent, accepted, and processing states, with the recipient able to accept, reject, or request changes. ### Slice [Section titled “Slice”](#slice) Deriving new [Threads](#thread) from a source Thread by copying or linking selected Fields, files, and Identifiers, with [Fabric](#fabric) recording the lineage. DUST Identifiers represent a physical identity and are never copied into a slice. ### Team [Section titled “Team”](#team) The access and collaboration scope within an [Organization](#organization). Teams own Threads and Folders, and are the unit of sharing, [Connections](#connection), and request context. The request header is `Dust-Ctx-Team-Id`; some API names keep the legacy “group” naming (`searchGroupIds`, and `Dust-Ctx-Grp-Id` as a still-accepted header alias). See [Conventions](/api/conventions/). ### Tamper Analysis [Section titled “Tamper Analysis”](#tamper-analysis) A comparison of a fresh scan of a DUST [Identifier](#identifier) against the reference captured when it was [bound](#bind), producing marker coverage measurements and visual evidence layers — and no conclusion. The platform reports what it measured and shows the evidence; it never states whether the Identifier was tampered with, and offers no summary number, rating, or threshold. See [Tamper Analysis](/use/tamper-analysis/). ### Tamper Observation [Section titled “Tamper Observation”](#tamper-observation) A person’s own conclusion drawn from one [Tamper Analysis](#tamper-analysis) — the only conclusion the platform stores. Its result is one of **Consistent**, **Expected** (normal wear and tear for the identifier’s use case and substrate), **Inconsistent**, or **Unknown**, chosen actively with no default. Observations are attributed, immutable, and never replaced: an Analysis retains the whole series. The subject is the Identifier surface, not the [Thread](#thread) or the goods it represents. ### Template [Section titled “Template”](#template) A reusable definition of the [Fields](#field) a [Thread](#thread) carries, used when creating Threads individually or importing them in bulk (for example from CSV). ### Thread [Section titled “Thread”](#thread) The core record of the platform: a digital identity for a physical thing or item. A Thread holds [Fields](#field), [Resources](#resource), [Identifiers](#identifier), and [Certificates](#certificate), lives in a [Folder](#folder), and can be classified, shared, shipped, sliced, and disclosed. ### Transfer [Section titled “Transfer”](#transfer) Moving [Threads](#thread) from one [Team](#team) to another over a [Connection](#connection): the recipient receives new Threads it owns outright, the source Threads are closed out, and [Fabric](#fabric) links the two sides. Surfaced in DICE as [Shipments](#shipment); acceptance is the point of no return. ### Verification [Section titled “Verification”](#verification) Confirming that a scanned [Identifier](#identifier) matches an expected Identifier bound to a specific [Thread](#thread) — a yes/no check against a claimed identity, as opposed to [Identify](#identify)’s open-ended search. See [Identifiers](/api/identifiers/). ### Void (Identifier) [Section titled “Void (Identifier)”](#void-identifier) Marking an [Identifier](#identifier) as no longer the live marking for an item — the DUST was destroyed, re-applied, or the marked material was cut away. Void is a label: the Identifier stays bound to its [Thread](#thread) with its full history, stays visible in the Identifier list (in red, marked **Voided**), and can still be identified, verified, and unbound. It is reversible, and it does not change the underlying DUST record. Distinct from archiving, which hides an Identifier from the default view, and from a [Certificate](#certificate) void, which is permanent. See [Identifiers and Scanning](/use/identifiers-and-scanning/). # Activity and transaction history > Audit everything that happens in DICE — the Team-wide Activity feed, grouped activities, filters, CSV export, and per-Thread transaction history. Every meaningful action in DICE — creating and editing Threads, binding and verifying identifiers, uploading files, sharing, shipments — is recorded as an event. Two surfaces expose this record: * the **Activity** page, a Team-wide feed of all events, and * **Transaction History** on each Thread’s detail page, scoped to that Thread. ## What an event records [Section titled “What an event records”](#what-an-event-records) Each event captures: * **Action** — what happened (e.g. “Created Thread”, “Bound”, “Verified”, “Uploaded File”, “Shared Thread”). * **Item** — the target: a Thread, folder, or shipment, linked so you can jump straight to it. * **User** — who did it (some events are recorded by the system rather than a person). * **Time** — when it occurred. For changes, events also carry the diff — expandable **Previous value** / **New value** details (“View changes”) — and, where available, richer context you can surface via CSV export or advanced mode: IP address, user agent, approximate location, and comments. ## The Activity page [Section titled “The Activity page”](#the-activity-page) Open **Activity** in the navigation. Events are listed newest-first; click a row to open its target (a Thread row deep-links to that exact event in the Thread’s history). Page through with **Newer** / **Older**, and adjust the page size (10–100 per page). ### Two views: Events and Groups [Section titled “Two views: Events and Groups”](#two-views-events-and-groups) A toggle at the top switches between: * **Events** — the flat event log, one row per event. * **Groups** — the same feed grouped into logical operations. A multi-step operation — a shipment, a slice, a CSV import, an assembly install — collapses under one header such as **Shipment**, **Slice**, **Imported Thread**, or **Assembly Position**, with a roll-up of how many actions and people it involved (“N actions”, “N people”). Single events still render as normal rows. Use Groups when you want “what happened”, Events when you want every individual record. ### Filtering [Section titled “Filtering”](#filtering) The filter bar narrows the feed: * **Action Type** — a multiselect of action categories: Bound, Bind Failed, Verified, Verification Failed, Identified, Identification Failed, Created Thread, Updated Thread, Created Field, Updated Field, Uploaded File, Document Verified, Archived Thread, Unarchived Thread, Viewed Thread, Created Folder, Updated Folder, and Deleted Folder. With no selection, **All Actions** are shown. * **From** / **To** — a date range. * **Show Viewed** — view events (“Viewed Thread”) are hidden by default (**Hide viewed**); switch to **Show viewed** to include them. Selecting the Viewed Thread action type includes them automatically. **Clear** resets all active filters. When filters are active, the page notes how many matching events are shown and whether older matching events are available. ### Exporting events to CSV [Section titled “Exporting events to CSV”](#exporting-events-to-csv) 1. Select events with the row checkboxes (the header checkbox selects the whole page; shift-click selects a range). 2. Click **Download CSV** in the selection bar. The CSV includes one row per selected event with full audit columns: event ID, action, title, occurred-at time, target type/ID/name, user ID/name/email, org and Team name, IP address, user agent, latitude/longitude, comment, and a summary of field changes. ### Advanced mode [Section titled “Advanced mode”](#advanced-mode) Inside the **Action Type** popover, an **Advanced mode** switch adds a copy button per row for the event’s UUID (**Copy event UUID**) — useful when referencing a specific event in a support request. The setting is remembered on your device. ## Transaction history on a Thread [Section titled “Transaction history on a Thread”](#transaction-history-on-a-thread) Each Thread’s detail page carries its own **Transaction History** — the same events, scoped to that Thread and its resources, shown as grouped activity blocks. A filter menu (**Filter Actions**) toggles between **Show All** and **Hide View Actions**. The history loads more as you scroll and ends with an explicit “End of transaction history” marker, so you always know you’ve seen everything. Deep links work here too: copying a link from an event (or arriving from the Activity page) scrolls the history to that exact event. ## Events on shared and disclosed Threads [Section titled “Events on shared and disclosed Threads”](#events-on-shared-and-disclosed-threads) History follows the Thread’s access rules. On Threads you can see through [sharing](/use/sharing-and-access/) or a [disclosure](/use/disclosures/), events whose details were not disclosed to you appear with a **Redacted** badge instead of their full content — you can see that something happened without seeing withheld values. Events that only concern content entirely hidden from you are omitted rather than shown redacted. Note Some event sources — shipments and slices — belong to modules whose availability depends on your organization’s configuration; if you don’t see the module in navigation it isn’t enabled (see [FAQ](/use/faq/)). ## Related pages [Section titled “Related pages”](#related-pages) [Threads](/use/threads/)Where per-Thread Transaction History lives. [Identifiers and scanning](/use/identifiers-and-scanning/)Bind, verify, and identify events explained. [Disclosures](/use/disclosures/)How disclosed history reaches other organizations. # Administrator guide > How organization admins invite users, manage Teams, set up cross-organization collaboration, and control module availability and API access. This guide is for **organization administrators** — the people responsible for who can sign in, which Teams exist, and how the organization collaborates with partners. Administration spans two surfaces: * **DICE** — the app itself, where day-to-day work happens and where Team admins manage [Connections](/use/connections/) and [sharing](/use/sharing-and-access/). * **The DUST Account portal** — the account-management site where organization membership, Teams, invitations, and API keys live. Open it from DICE via the user menu (top right) → **Account Management**. Note Some organization-management surfaces depend on your organization’s configuration. If an area described here isn’t visible to you, see the [FAQ](/use/faq/) and contact [](mailto:support@dustidentity.com). ## Inviting users [Section titled “Inviting users”](#inviting-users) User accounts and organization membership are managed in the **DUST Account portal**, not in DICE. New members join by invitation: 1. Open the account portal (user menu → **Account Management**) and go to your organization’s **Invitations** section. 2. Click **Invite User**. In the **Create Invitation** dialog, enter the invitee’s **email address**, choose their organization role (**Member** or **Admin**), and optionally assign them to a **Team** right away. 3. Click **Send Invitation**. The invitee receives an email with an **Accept Invitation** link. 4. The invitee follows the link, signs in (or creates their DUST account), and accepts. They then appear in your organization’s member list. Keep in mind: * **Invitations expire after 48 hours.** From the invitations list you can **resend** an invitation (which refreshes its expiry) or cancel a pending one. * Organization admins cannot create new organizations themselves — organizations are provisioned by DUST Identity. * Existing members are managed from the organization’s **Users** section: change a member’s role or remove them from the organization. ### What a brand-new user sees [Section titled “What a brand-new user sees”](#what-a-brand-new-user-sees) A user who signs in to DICE **before belonging to any organization and Team** is stopped at the context-selection screen with the message **“No selectable org/team contexts are available for this account yet.”** and a **Sign out** button. Once you’ve invited them and placed them on a Team, they can sign in again (or reload) and pick their organization and Team. See also the [FAQ](/use/faq/). ## Teams [Section titled “Teams”](#teams) Everything a user does in DICE happens in the context of an **organization and a Team** — Threads, folders, shipments, and other data belong to a Team, and users switch context from the header. A user must be on at least one Team to use DICE. Team management also lives in the **account portal**, under your organization’s **Teams** section: * **Create a team** — organization admins only (“Only organization admins can create teams”). A team needs just a name. * **Add members** — pick from existing organization members (search by name or email). Invite people to the organization first, then place them on Teams. * **Team roles** — each team member is either **Member** (“Standard team access”) or **Admin** (“Manage team members”). Team admins can manage their own team’s membership without organization-admin rights. * **Publisher** — an additional grant on a team membership, given by a team admin. It is not a third rung above Member: a member either holds it or doesn’t, and Team admins always do. Publisher is what gates making the team’s data public — every change in the [Public Pages](/use/public-pages/) module (building and publishing designs, publishing or unpublishing a page, rolling a version out, running or cancelling a Publish Wave) requires it. Members without it can still open the module and read designs, version history, published pages, and page activity. Service Accounts can hold the grant too, so an integration can publish without a person driving it. * **Rename or remove** — from the team’s settings. Team admins get extra capabilities inside DICE as well: the Team admin section of the sidebar (for example **Connections**) only appears for admins of the active Team. Tip Invites and member management live in the account portal — if you’re looking for “add user”, that’s where it is. ## Setting up cross-organization collaboration [Section titled “Setting up cross-organization collaboration”](#setting-up-cross-organization-collaboration) Collaboration with another organization starts with a **Connection** between one of your Teams and one of theirs. Establishing a connection is an admin task — the confirmation handshake is performed by Team admins on both sides. See [Connections](/use/connections/) for the full flow, including connection direction (who may send and who may receive). Once a connection is confirmed: * Teams can share Threads and folders with the partner Team — see [Sharing and access](/use/sharing-and-access/). * Teams can send [Shipments](/use/shipments/) to transfer Threads to the partner organization, where the connection’s direction allows it. Note Connections availability depends on your organization’s configuration (see the [FAQ](/use/faq/)). ## Module availability [Section titled “Module availability”](#module-availability) DICE is modular. Capabilities such as **Shipments**, **Certificates**, **Vlinks**, **Slicing**, **Public Pages**, **Connections**, and **Sharing** are enabled **per organization by DUST Identity** — they are not switches an organization admin can flip in the product. When a module is disabled for your organization, its navigation entries are hidden entirely; users won’t see a locked or greyed-out item. If your team asks “where is feature X?”, check whether the module is enabled before troubleshooting further. To change which modules are enabled for your organization, contact [](mailto:support@dustidentity.com). ## API access [Section titled “API access”](#api-access) Programmatic access to the DUST platform uses **Service Accounts** — machine identities owned by your organization — managed in the **account portal** (organization page → **Service accounts** tab). A Service Account belongs to exactly one organization, can be granted team access like a member, and every action it performs is attributed to it in activity history. Creating and managing Service Accounts requires both: 1. You are an **admin** of the organization, and 2. **Service accounts are enabled** for that organization by DUST Identity. If they aren’t, the tab shows a prompt to contact [](mailto:support@dustidentity.com) to request access. Each Service Account can hold **API keys** and/or an **OAuth client** for `client_credentials`. Credential secrets are shown **once** at creation — copy them immediately. Several credentials can be active at the same time, so rotation needs no downtime; each credential’s last-used time is visible in the portal. Disabling a Service Account immediately stops it from obtaining new tokens. For traceability, a Service Account’s **operator attribution** policy can be set to *required*, which forces its API writes to declare which person initiated each action (for example, a shop-floor badge id) — see [API authentication](/api/authentication/#declared-actor-attribution). See [API authentication](/api/authentication/) for how to authenticate API requests, and [Environments](/reference/environments/) for environment URLs. ## Quick reference [Section titled “Quick reference”](#quick-reference) | Task | Where | | --------------------------------------- | ------------------------------------------------------------------------------------------------------ | | Invite a user, manage members and roles | DUST Account portal → organization → Users / Invitations | | Create Teams, manage team membership | DUST Account portal → organization → Teams | | Connect with a partner organization | DICE → **Connections** (Team admins; see [Connections](/use/connections/)) | | Share Threads or folders across Teams | DICE (see [Sharing and access](/use/sharing-and-access/)) | | Enable or disable modules | Contact [](mailto:support@dustidentity.com) | | Service Accounts and API credentials | DUST Account portal → organization → Service accounts (see [API authentication](/api/authentication/)) | # Assemblies > Build multi-part structures by attaching threads to an assembly, organize them with named positions, and see the full tree. Note Assemblies availability depends on your organization’s configuration — if you don’t see **Assemblies** in navigation, the module isn’t enabled (see the [FAQ](/use/faq/)). An **Assembly** is a thread that can contain other threads. Use one to model anything built from parts — an engine holding its components, a kit holding its contents, a machine holding its installed modules. The assembly is still a full thread: it has its own name, fields, files, identifiers, and history, plus a list of **attached Threads**. Attached threads remain independent threads. Attaching doesn’t merge or copy anything — each part keeps its own record, and you can open it, edit it, or detach it at any time. ![An assembly thread's detail page with an Assembly card listing its two attached part threads](/_astro/assembly.DIN140Ar_ZwWAJL.webp) An assembly is a thread with parts: the Assembly card lists the attached threads, positions, and the rolled-up BOM. ## Turn a thread into an assembly [Section titled “Turn a thread into an assembly”](#turn-a-thread-into-an-assembly) Any thread you own can become an assembly: 1. Open the thread. 2. Open the **“⋯”** overflow menu in the top right (labeled **More actions**). 3. Choose **Convert to Assembly**. The thread now shows an **Assembly** badge and gains the attachment tools described below. Nothing else about it changes. To convert an assembly back, choose **Convert to Thread** from the same menu. This is only allowed once the assembly is empty — the menu item stays visible but disabled with the reason inline (for example **Detach 2 attached Threads and clear 1 position first**) until you detach all attached threads and clear all named positions. ## Attach and detach threads [Section titled “Attach and detach threads”](#attach-and-detach-threads) From the assembly’s detail page: 1. Click **Attach Thread**. 2. In the **Attach a Thread** dialog, select one or more threads your team owns. 3. Click **Attach selected**. An assembly can contain other assemblies — nested ones appear with a **Sub-assembly** badge, so you can model multi-level structures. To remove a part, open the attachment’s actions menu and choose **Detach Thread** (or **Detach Assembly**). Detaching does not delete anything: the thread is removed from the assembly, keeps its own access, and remains in your workspace. If the thread was assigned to named positions, detaching clears those position references — the confirmation dialog tells you how many. Each attached thread also gains a **Part of** section on its own detail page, showing the parent assembly it is installed in — outside its own contents. ## Positions [Section titled “Positions”](#positions) Positions are named slots that say *where* each part goes — **Left rotor**, **Slot A**, a role, a location. They’re optional: attach threads first, then assign positions when location matters. * **Add position** creates a named slot. A position can exist without a thread (“**Unassigned**”) until one is assigned. * Each position can point at one attached thread (its **Linked Thread**). Assign one when creating the position, or later with **Assign a Thread**. * The same thread may be assigned to more than one position — the dialog warns you when you’re about to do that, so duplicates are deliberate. * Use **Edit position** to rename a slot and **Clear assignment** to empty it without deleting the position. The assembly card on the thread page has three tabs: **Threads** (the flat attachment list), **Positions** (the named slots and what fills them), and **Hierarchy** (the nested tree). ## Protected attachments [Section titled “Protected attachments”](#protected-attachments) By default, people who can see the assembly can also see its attached threads through it (see [access inheritance](#assemblies-and-sharing) below). When a part is sensitive, protect it: * From the attachment’s actions menu, choose **Protect attachment**. The row shows a **Protected attachment** badge. * A protected attachment stays in the assembly but does not inherit the assembly’s access — sharing the assembly won’t expose it. People with access to the assembly can’t see the protected part through that attachment. * Choose **Remove protection** to make it inherit access again. ## The Assemblies page [Section titled “The Assemblies page”](#the-assemblies-page) The **Assemblies** entry in the navigation opens a workspace-wide explorer: “Explore your Assemblies, attached Threads, nested Assemblies, and full attachment trees.” Pick an assembly from the **All assemblies** list, then browse its **Attached Threads** or its **Full tree**. ## Rolled-up tree and graph [Section titled “Rolled-up tree and graph”](#rolled-up-tree-and-graph) For deep structures, the **Full tree** view rolls up every attachment, recursively through sub-assemblies. Switch between: * **Tree view** — an expandable/collapsible outline of the whole structure. * **Graph view** — a containment graph. Use **Expand graph** for a full-screen version: drag to pan, scroll to zoom, and click an attached thread to open it. Each node shows summary counts for the part’s fields, files, and identifiers. Very large trees are truncated to the first attachments with a warning, and parts you don’t have access to are simply not shown (“No visible attached Threads”). ## Assemblies and sharing [Section titled “Assemblies and sharing”](#assemblies-and-sharing) Sharing an assembly shares its contents by default: teams and users you grant access to can see the attached threads *through* the assembly, without a separate share per part. Two consequences worth knowing: * **Detaching can remove access.** If someone could see a part only through the assembly, detaching that part takes their visibility with it — the detach confirmation calls this out. * **Protected attachments never inherit.** Use [protection](#protected-attachments) for parts that must not travel with the assembly’s access. See [Sharing and threads](/use/threads/) for how thread access works in general. ## Assemblies and shipments [Section titled “Assemblies and shipments”](#assemblies-and-shipments) When you add an assembly to a shipment, its parts go with it — the shipment composer shows a **Parts** panel where you can review the whole tree and exclude parts: * “Excluding a part leaves out everything installed in it. Excluded parts stay with your team and can be transferred separately.” * A thread that is listed in the shipment on its own *and* inside an assembly transfers only once — the receiving team gets one copy. * Parts that were already shipped in a previous shipment can’t ship again. The composer asks you to resolve each one: detach it from the assembly (permanent) or skip it (it stays attached, this shipment just won’t include it). After a source thread has been shipped, DICE also warns you before attaching, detaching, or assigning it inside an assembly — those edits change only your team’s source-side assembly record and have no effect on the recipient’s copy. See [Shipments](/use/shipments/) for the full transfer flow. ## Importing an assembly package [Section titled “Importing an assembly package”](#importing-an-assembly-package) Note Assembly import availability depends on your organization’s configuration. If the **Import package** button is missing from the Assemblies page, it isn’t enabled for you (see the [FAQ](/use/faq/)). **Import package** on the Assemblies page (or `/app/assemblies/import`) accepts an Assembly Import Package — a JSON file describing threads, assemblies, attachments, and links. The package is validated into a plan first: nothing is written until you click **Commit import**, and the import is all-or-nothing. The plan preview shows exactly what will be created or reused, plus any blocking issues to resolve. For other ways to bring data in, see [Importing](/use/import/). # Categories > Group threads across folders and assemblies with additive, nestable categories. Note Categories availability depends on your organization’s configuration — if you don’t see **Categories** in navigation, the module isn’t enabled (see the [FAQ](/use/faq/)). Categories are “cross-cutting groups that classify threads across folders and assemblies. A thread can belong to many categories at once.” ## Categories vs. folders [Section titled “Categories vs. folders”](#categories-vs-folders) The two answer different questions: * A **folder** is a thread’s single home — every thread lives in exactly one folder. See [Folders](/use/folders/). * A **category** is an additive label — “Categories are additive, cross-cutting groupings — a thread can belong to several at once. This is distinct from a folder (single-home).” Use folders for where a thread lives, and categories for what it is: the same thread can sit in the folder `Line 3 / June builds` while belonging to the categories `Titanium parts` and `Customer X program` simultaneously. ## Browse the category tree [Section titled “Browse the category tree”](#browse-the-category-tree) Open **Categories** in the sidebar. The page shows a **Categories** sidebar with the tree — expand and collapse nested categories, or filter with **Search categories…** — and a threads panel for the selected category showing its thread count and members. On small screens the tree becomes a **Select a category** sheet. If your team has none yet, the empty state explains: “Create a category to group threads across folders and assemblies. Categories can nest — make an outer category, then add subcategories inside it.” ## Create categories and subcategories [Section titled “Create categories and subcategories”](#create-categories-and-subcategories) 1. Click **New** in the Categories sidebar (or **New category** from the empty state) to open the **New category** dialog. It reminds you that “Categories are additive, cross-cutting groupings — a thread can belong to several at once.” 2. Enter a **Name** (e.g. “Playing cards”) and an optional **Description** (“What does this category group together?”), then click **Create**. 3. To nest, open a category’s actions menu and choose **New subcategory**. The **New subcategory** dialog explains that nesting “lets you build an outer/inner structure (e.g. Cards → Diamonds, Clubs). A thread can still belong to several categories at once.” ## Add threads to a category [Section titled “Add threads to a category”](#add-threads-to-a-category) There are three ways in: * **From the category** — in the selected category’s threads panel, click **Add threads**. The picker (“Add threads to *name*”) lets you “Select one or more threads your team owns to add to this category”, then confirm with **Add to category**. DICE reports the outcome, e.g. “3 threads added to category — 1 already in it”. * **From a thread** — on a thread’s page, use the inline **Category** button to open the **Add to category** dialog. Search existing categories, or type a new name and create one on the spot (**New category**) — the toast confirms “Added to category” or “Category created and added”. * **From Folders** — select threads in the Folders view and choose **Add to → Add to category**, as the category empty state suggests. ## Remove threads from a category [Section titled “Remove threads from a category”](#remove-threads-from-a-category) On a thread’s page, each category chip has a remove control (“Remove from *name*”). Removing a thread from a category never moves or deletes the thread — it only detaches the label. ## Move a category [Section titled “Move a category”](#move-a-category) From a category’s actions menu, choose **Move…** to open the **Move category** dialog and “Choose a new parent” for it, “or move it to the top level.” The dialog offers **Top level (no parent)** and marks ineligible destinations with hints: “This category”, “Can’t move into its own subcategory”, “Current parent”, “Owned by another team”, and “View only”. ## Share a category [Section titled “Share a category”](#share-a-category) Categories have **Share** and **Access** actions like folders do — see [Sharing and Access](/use/sharing-and-access/). ## Find uncategorized threads [Section titled “Find uncategorized threads”](#find-uncategorized-threads) The thread **Search** page can filter to **Uncategorized** — threads that belong to no category yet — which is useful for keeping classification complete. Note The **Uncategorized** filter’s availability depends on your organization’s configuration — if you don’t see it among the thread filters, it isn’t enabled for your organization (see the [FAQ](/use/faq/)). # Certificates > Design Certificate Forms, run preflight checks, and generate immutable PDF certificates from Thread data — one at a time or in batches. Certificates are **immutable PDFs generated from a Thread’s data**. You design a reusable **Certificate Form** once — a PDF or image background with placed data zones — and then generate certificates from it for any Thread whose data satisfies the form. Once issued, a certificate is a permanent copy: editing the Thread or the form afterwards never changes an already-issued PDF. Note Certificates availability depends on your organization’s configuration. If you don’t see **Certificate Forms** in the sidebar, the module is not enabled for your organization (see the [FAQ](/use/faq/)). ## Who can do what [Section titled “Who can do what”](#who-can-do-what) Access follows your role in the active Team: * **Team admins** manage forms: creating, editing the design and field bindings, importing, and archiving them. * **All team members** can browse forms and use active ones to generate certificate PDFs from Thread data. ## Certificate Forms [Section titled “Certificate Forms”](#certificate-forms) Open **Certificate Forms** from the sidebar (`/app/certificates/forms`). Forms are “reusable designs that bind Thread data into generated Certificate PDFs.” The list shows each form’s name, status, binding count, and last update; use **Show archived** to include archived forms. From the list you can create a form (**New Form** — give it a name; you’ll add the design next), **Copy form**, **Import** a previously exported form definition, and **Archive** or **Restore** a form. A form’s status is either **Active** or **Archived** — only active forms are offered when generating. ### The form designer [Section titled “The form designer”](#the-form-designer) Opening a form launches the **Certificate Form Designer**. A form has two ingredients: 1. **A design** — upload a PDF or image (**Upload design (PDF or image)**) to use as the certificate background. A design is required before you can place fields. Replacing the design later removes all placed fields and their positions; already-issued certificates are unaffected. 2. **Placed fields** — zones you click onto the design, each bound to a data source. To see realistic values while designing, pick a **Design Context** (“Designing against”): either one Thread (real preview data) or one Thread Template (field names only) to seed bindings from. Switching context never changes fields you’ve already placed. ### Field sources [Section titled “Field sources”](#field-sources) Each placed zone pulls its value from one of four sources: * **Thread fields** — chips listing the fields of the Thread or Template you’re designing against. Fields already covered by the form are marked, and you can add a field name as an **alias** to an existing zone instead of placing it again. * **System sources** — values DICE fills in at generation time: **Thread name**, **Thread description**, **Thread thumbnail**, **Thread UUID**, **Thread identifiers**, **Issue date**, **Issuer team name**, and the issuer’s first, family, and full name. System sources can also be attached as **fallbacks** to a field zone, “used in order when no Thread field matches.” * **Custom field** — type a field name (plus optional aliases and a kind) for data the design context doesn’t show yet. * **Static text** — literal text, “printed exactly as written — not pulled from the thread.” Field kinds are **Text**, **Number**, **Date**, **Boolean**, and **Image**, and a zone can render as plain **Text** or as a code: **QR**, **Code 128**, **Code 39**, or **PDF417**. Zones have position/size in millimetres, font, size, color, alignment, rotation, opacity, and (for dates) a date format. ### Stable Vlink QR zones [Section titled “Stable Vlink QR zones”](#stable-vlink-qr-zones) Add the **Vlink URL** system source when a Certificate needs a permanent QR code. You may place it more than once. Each placement receives its own Vlink when the Certificate is generated, so two QR zones can lead to different destinations and be managed independently afterward. During generation, DICE shows one configuration card for every Vlink placement. Each card can point to the Thread’s Public Page, use a custom HTTP(S) URL, or stay unconfigured as a draft. The Public Page is selected by default when one exists, but it is not required for the other placements. Preview QR codes are representative and distinct; their permanent Vlink URLs are created only when you click **Generate**. Note Certificate Vlink QR zones remain available whenever the Certificates module is enabled. The separate Vlinks administration module controls whether Team admins see the **Vlinks** inventory in the sidebar; it does not prevent certificate generation from creating the stable links needed by a form. ### How fields resolve — by name, not by template [Section titled “How fields resolve — by name, not by template”](#how-fields-resolve--by-name-not-by-template) A form is **not** tied to a Thread Template. At generation time, each zone’s field name and aliases are matched against the Thread’s field names **case-insensitively** — “Name”, “name”, and “NAME” all resolve to the same field. Any Thread whose field names match can use the form, regardless of which template (if any) created it. Marking a zone **Required** “blocks generation when this value cannot resolve.” Non-required zones simply render empty when nothing matches. While a Thread is selected as the design context, the preview flags zones that won’t resolve, with reasons such as *no value*, *type mismatch*, *ambiguous*, *won’t resolve*, or *won’t encode* (for code formats), and a banner listing required fields that “won’t resolve against this Thread.” ## Preflight [Section titled “Preflight”](#preflight) Whenever you pick a form for a Thread, DICE runs a **preflight**: it reports how many required fields resolve (“Resolves 3/4 required fields”) and an overall verdict of **Compatible** or **Not compatible**. Fields satisfied through an alias are marked with the alias that matched. Generation is only allowed when the preflight passes. ## Generate a certificate for a Thread [Section titled “Generate a certificate for a Thread”](#generate-a-certificate-for-a-thread) 1. Open the Thread and find its **Certificates** card (“Immutable PDFs generated from this thread’s data.”). 2. Click **Generate Certificate**. In the dialog, pick a **Certificate Form** — or choose **+ New form from this thread…** to create a form seeded with this Thread’s fields. 3. Review the preflight result for the selected form against this Thread. 4. Optionally set a **Certificate name** — it is “shown in the certificates list and used as the download filename” and defaults to the form name. If the form contains Vlink QR zones, configure each destination independently. Click **Preview certificate** to inspect the rendered PDF first. 5. Click **Generate**. On success you’ll see **Certificate issued**, and the certificate appears on the card with its issue date. From the card, each certificate offers **Download** (the PDF) and **Void**. ## Batch generation [Section titled “Batch generation”](#batch-generation) To issue the same certificate for many Threads at once: 1. In the [Threads](/use/threads/) list, multi-select the Threads and choose **Generate Certificates**. This opens **Batch Generate Certificates** (`/app/certificates/batch`) with your selection queued. A batch is limited to **100 threads**. 2. Select a Certificate Form. DICE preflights every queued Thread and labels each one **Compliant**, **Not compliant** (with the missing fields listed), or **Not viewable** (you can’t view that Thread in this context). Use the re-check action after fixing Thread data. 3. Click **Issue N compliant**. Certificates are “issued one at a time”; each row moves through **Issuing…** → **Issued** or **Failed**. 4. Afterwards, use **Retry N failed** for failures, remove rows with **Remove from batch**, and **Export CSV** for a record of the run. ## Voiding a certificate [Section titled “Voiding a certificate”](#voiding-a-certificate) Issued certificates cannot be edited or deleted — they can be **voided**. “Voiding marks the certificate as no longer valid to rely on. The PDF and its hash are unchanged.” 1. On the Thread’s Certificates card, click **Void** next to the certificate. 2. Pick a **Reason**: **Issued in error**, **Incorrect thread data**, **Wrong form**, **Duplicate certificate**, or **Other**. 3. Optionally add an **Internal note (private)**, then confirm with **Void**. Voided certificates stay in the list marked **Voided**, and remain downloadable for your records. ## Form lifecycle tips [Section titled “Form lifecycle tips”](#form-lifecycle-tips) * **Archive** retires a form without touching certificates already issued from it; **Restore** brings it back. Archived forms don’t appear in the generate dialog. * **Copy form** duplicates a form (including its design and bindings) so you can iterate without disturbing the original. * **Export** a form from the designer and **Import** it from the forms list to move a design between Teams — a form needs a design and a name before it can be exported. ## Related pages [Section titled “Related pages”](#related-pages) * [Threads](/use/threads/) — the data certificates are generated from * [Getting started](/use/getting-started/) — orientation for new users * [FAQ](/use/faq/) — module availability and troubleshooting # Connections > Link your team with a team in another organization so you can share data and send shipments across organization boundaries. A connection is a team-to-team link between two **different organizations**. It is the prerequisite for everything cross-org in DICE: [sharing](/use/sharing-and-access/) a thread or folder with a partner team and sending them [shipments](/use/shipments/) both require an active connection whose direction allows the data to flow. Connections live on the **Connections** page: *“Pair your teams with external organizations using invite codes.”* Note Connections availability depends on your organization’s configuration — if you don’t see **Connections** in navigation, the module isn’t enabled for your team (see the [FAQ](/use/faq/)). Creating, accepting, and confirming connections is limited to team admins. ## How connections work [Section titled “How connections work”](#how-connections-work) Connections are established with a secure, invite-code-based handshake — *“A secure handshake between two organizations — no emails sent by DICE.”* The Connections page summarizes it in four steps: 1. **“You create the invite”** — *“DICE generates a link and QR code. It does not send an email.”* 2. **“You share the link yourself”** — *“Send the link over email, Slack, or any channel you trust.”* 3. **“Recipient accepts”** — *“A partner admin opens the link and accepts on behalf of their team.”* 4. **“You confirm to finalize”** — *“Come back here to confirm — the connection goes live on both sides.”* Because both an explicit accept **and** a confirm are required, neither side can be connected unilaterally. ## Create an invite [Section titled “Create an invite”](#create-an-invite) 1. On the **Connections** page, click **Create Invite**. 2. Optionally set **“Restrict to email”** (marked **Recommended**): *“If set, only the user signed into DICE with this exact email will be able to accept the invite.”* Even if someone else gets the link, they can’t accept it. Without a restriction, *“Any DICE admin who receives this link can accept it on behalf of their team.”* 3. Choose the **“Data flow direction”** — see [Direction](#direction) below. 4. Click **Generate Invite Link**. DICE shows the invite link and a QR code. As the dialog notes: **“DICE won’t email the invite for you”** — *“it’s up to you to send it to the recipient over whatever channel you trust — email, Slack, a phone call, etc.”* 5. **Copy** the link and send it to the partner admin yourself. ## Accept an invite (partner side) [Section titled “Accept an invite (partner side)”](#accept-an-invite-partner-side) The recipient opens the link while signed into DICE and sees the **Connection Request**: * Only a **team admin** can accept: *“Accepting it joins your team to another organization’s team, so only an admin of the receiving team is allowed to do it.”* * The invite can’t be accepted from the **same organization** that sent it — invites only connect two separate organizations. * The recipient picks which of their admin teams should receive the connection, then chooses **Accept**, **Reject**, or **Decide Later**. Accepting is not the end: *“Accepting won’t activate the connection yet — the requester still has to confirm on their end before data flows.”* ## Confirm the connection (requester side) [Section titled “Confirm the connection (requester side)”](#confirm-the-connection-requester-side) Back in your Connections table, the invite’s status flips to **Accepted**. Click **Confirm** to finalize — the connection becomes **Connected** and is active for both parties. ## Statuses [Section titled “Statuses”](#statuses) | Status | Meaning | | ------------- | ----------------------------------------------------------------------------- | | **Pending** | Invite created; waiting for the recipient to accept. | | **Accepted** | The partner accepted; waiting for you to **Confirm**. | | **Connected** | Active — sharing and shipments can flow (subject to direction). | | **Paused** | Temporarily suspended by one side; shares and in-flight shipments are frozen. | | **Rejected** | The recipient declined the invite. | | **Canceled** | The invite was withdrawn before acceptance. | ## Direction [Section titled “Direction”](#direction) Every connection has a data-flow direction, set when the invite is created: *“Control which direction thread data flows between the two teams.”* From the creating team’s perspective the options are: * **Send** — *“Send thread data to the connected team.”* * **Receive** — *“Receive thread data from the connected team.”* * **Send & Receive** — *“Send and receive thread data with the connected team.”* Each side sees the direction from its own point of view (**“Send to them”** / **“Receive from them”** / **“Send & Receive”**). Direction gates both sharing and shipments: if the connection doesn’t allow your team to send to the partner, you can’t share items with them or pick them as a shipment recipient, and existing shares to them are suspended (see [Sharing and access](/use/sharing-and-access/)). ### Change the direction [Section titled “Change the direction”](#change-the-direction) Direction changes use the same double-confirmation as the original handshake. From the connection’s row, choose **Change connection direction**: *“Propose a new data-flow direction. The other team must accept and you must confirm before it takes effect — the current direction stays in force until then.”* 1. You pick the **“New direction”** and click **Propose change**. The row shows **“Change pending →”** with the proposed direction. 2. The other team accepts the change (*“Direction change accepted — awaiting their confirmation”*). 3. You confirm (*“Direction change confirmed”*) — only then does the new direction take effect. Either side can cancel a pending change before it’s confirmed. If the new direction disallows existing shares, those shares are suspended until the direction allows them again. ## Pause and resume [Section titled “Pause and resume”](#pause-and-resume) Pausing is a reversible freeze. The confirmation dialog (**“Pause this connection?”**) spells out the consequences: the shared items between your team and the partner *“will be suspended — the other team loses access until you resume, and resuming restores everything automatically”*, any in-flight shipments are frozen, and *“Nothing is deleted.”* Resuming (**Resume connection**) restores everything: *“Connection resumed — suspended shares restored.”* ## Delete [Section titled “Delete”](#delete) Deleting a connection is permanent. The confirmation dialog (**“Delete this connection?”**) warns: *“This permanently revokes all share grants between your team and the partner. … Reconnecting later starts sharing from zero. This cannot be undone.”* In-flight shipments are canceled — but *“Threads either team received through completed shipments are theirs and are not affected.”* Pending invites can be canceled or deleted from the same table without any of these consequences. ## Related [Section titled “Related”](#related) [Sharing and access](/use/sharing-and-access/)Grant a connected team viewer or editor access to threads and folders. [Shipments](/use/shipments/)Transfer threads to a connected team in another organization. # Core concepts > The DICE mental model in one page — Threads and Identifiers, how you organize them, how teams share and ship them, and how Fabric keeps provenance. DICE connects physical objects to trusted digital records. Everything in the platform hangs off one idea: **each physical item gets exactly one digital record, called a Thread**, and a physical **Identifier** on the item ties the two together. The rest of the vocabulary — Folders, Categories, Assemblies, Connections, Shipments, Fabric, Disclosures — describes what you do with Threads: how you organize them, how you work on them with other teams, and how their history survives as they move between organizations. This page gives you the whole map. Each section links to the full guide for that area. The DICE noun map: an Identifier binds a physical item to its Thread; Folders, Categories, and Assemblies organize Threads; Sharing and Shipments move data between Teams over Connections; Fabric keeps the provenance chain. ## One item, one Thread [Section titled “One item, one Thread”](#one-item-one-thread) A **[Thread](/use/threads/)** is the digital record for one physical item — a part, a lot, a serialized unit, a finished product. It holds: * **Fields** — typed data (text, numbers, dates, files, and more), kept consistent across similar items by **Templates**. * **Files** — documents, images, and other media. * **Identifiers** — the physical marks bound to the item. * **Transaction history** — every event that ever happened to the Thread, permanently. The physical link comes from **[Identifiers](/use/identifiers-and-scanning/)**: a DUST mark, QR code, barcode, data matrix, or NFC tag bound to the Thread. Scanning an identifier resolves back to its Thread — that’s how you identify an unknown item or verify an item is what it claims to be. Threads belong to a **team** within your **organization**. Teams scope everything: ownership, visibility, and permissions. ## Organizing Threads [Section titled “Organizing Threads”](#organizing-threads) Three structures organize Threads, and they answer different questions: | Structure | Question it answers | Shape | | ---------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------- | | **[Folder](/use/folders/)** | Where does this Thread live? | Each Thread has exactly one home folder. Folders nest. | | **[Categories](/use/categories/)** | What kinds of thing is it? | A Thread can carry any number of category labels. | | **[Assembly](/use/assemblies/)** | What is it physically built from? | An Assembly is itself a Thread whose parts are other Threads — a bill of materials. | **[Relationships](/use/relationships/)** cover everything looser: free-form links between related Threads, like a part and its batch record. ## Working with other teams [Section titled “Working with other teams”](#working-with-other-teams) Cross-organization work starts with a **[Connection](/use/connections/)** — the standing channel between your team and a team in another organization. A Connection has a lifecycle and a direction (send, receive, or both) that governs everything flowing between the two teams. Over a Connection, there are two very different ways to give another team your data — this is the distinction that trips up most new users: Sharing and shipping both move data over a Connection, but they are different acts: a share is a window into your live Thread; a shipment hands the receiving team their own copy. * **[Sharing](/use/sharing-and-access/)** grants live access to the Thread you own. There is one record; the other team views or edits it in place; you can revoke the share at any time. * **[Shipments](/use/shipments/)** transfer a copy. You choose exactly which fields, files, and identifiers travel; the receiving team reviews the shipment and, on acceptance, gets their own Threads. Your originals stay behind, marked **Shipped**. Rule of thumb: **share to collaborate, ship to hand over.** ## Provenance: where things came from [Section titled “Provenance: where things came from”](#provenance-where-things-came-from) When items physically move or change hands, their history shouldn’t evaporate. Three features keep it: * **[Slicing](/use/slicing/)** derives new Threads from an existing one *within your team* — cutting rods from a bar, splitting a lot into serialized units. The source is unchanged; the derivation is recorded. * **[Fabric](/use/fabric/)** is the lineage graph those derivations build. Every shipment and every slice leaves a permanent link, so a Thread can be traced upstream to its origins and downstream to everything made from it — across organizations, independent of whether you are still connected. * **[Disclosures](/use/disclosures/)** control what travels along that graph afterward. Transferred copies are snapshots; when a source team wants downstream holders to see new or corrected data, it explicitly *pushes* a disclosure, and every downstream team can review and adopt it. What is withheld simply never appears. **[Certificates](/use/certificates/)** round this out: immutable, point-in-time PDFs generated from a Thread’s data, for when provenance needs to leave the platform entirely. Note Several of these areas — Sharing, Connections, Shipments, Slicing, Fabric, Disclosures, Certificates — are modules enabled per organization by DUST Identity. If you don’t see one in your navigation, it isn’t enabled for your organization (see the [FAQ](/use/faq/)). ## Where next [Section titled “Where next”](#where-next) [Getting started](/use/getting-started/)Sign in, create your first folder and Thread, and bind your first identifier. [Glossary](/reference/glossary/)Every platform term, defined — the reference behind this page. [Identifiers and scanning](/use/identifiers-and-scanning/)DUST marks, QR, barcodes, and NFC — identify and verify physical items. [Build with the API](/api/core-model/)The same concepts, through the DUST API. # Disclosures and updates > Push chosen assets down a thread's Fabric chain, and review, adopt, or skip what upstream sources disclose to you. When a thread has downstream derivations — copies made by [shipments](/use/shipments/) or [slices](/use/slicing/) — its owner controls what those downstream teams can see of it. That choice is a **disclosure**: the source team selects which fields, files, and identifiers flow down the [Fabric](/use/fabric/) chain. What is disclosed is shown faithfully; what is withheld simply doesn’t appear downstream. Disclosure runs in both directions for you: * **As a source**, you *push* disclosures from threads you own. * **As a recipient**, disclosed changes arrive as *updates* you can review and bring into your own thread. What is disclosed is shown faithfully to every downstream team; what is withheld simply doesn't appear. Recipients choose whether to adopt disclosed changes into their own copy. Note These features ride on the Fabric surface, whose availability depends on your organization’s configuration (it is enabled when Shipments or Slicing is enabled). If **Updates** is missing from your navigation, it isn’t enabled (see the [FAQ](/use/faq/)). ## Pushing a disclosure [Section titled “Pushing a disclosure”](#pushing-a-disclosure) You push from a source thread you own — from its Fabric explorer’s **Push disclosure** action, or the same button on the thread’s **Lineage** card. This opens the **Push disclosure** page. 1. **Choose what to disclose.** The left side lists the thread’s assets by section — **Positions**, **Data**, **Files**, **Identifiers** — with **All** / **None** shortcuts. Items already disclosed show a **Disclosed** badge; changed ones show **New** or **Updated**. Private items can’t be selected (“Private — can’t be disclosed”). 2. **Preview.** The right side shows what downstream consumers will see through this thread’s fabric chain — exactly the disclosed rendering, nothing more. 3. Optionally toggle **Advance cursor** to reveal the thread’s recent events downstream. You can also push with no asset changes at all — a “cursor-only push — reveals recent events, no asset changes.” 4. Optionally add a **Reason** — it is recorded on the Fabric timeline. 5. Click the push button (for example **Push 3 assets**). On success: “Disclosure pushed — downstream consumers can see the update.” Disclosure is chain-wide As the page itself warns: “Disclosure flows the whole way down the Fabric chain — everyone downstream sees this. Once disclosed, it can’t be un-seen, only redacted later.” ### Revising later [Section titled “Revising later”](#revising-later) Push again whenever the thread changes — the composer marks what’s **New** or **Updated** since your last disclosure. An asset you take back out of the disclosure appears downstream as **Removed** (“removed from this link — no longer disclosed”) or **Redacted** (“redacted by the source — details withheld”); downstream teams keep any local copy they already adopted, but see no further detail. ## Receiving updates from your sources [Section titled “Receiving updates from your sources”](#receiving-updates-from-your-sources) When an upstream source pushes a disclosure to a thread you own, DICE surfaces it as an update in three places: * **On the thread** — an updates panel showing “Updates waiting for your decision”, with a per-source view of everything disclosed to this thread. * **The thread’s Updates page** — **Updates for \**: “Review what your sources disclosed and choose what to bring into this thread.” * **The app-wide Updates feed** — the **Updates** page in the navigation lists every thread of yours with pending updates (“New information disclosed by sources of your threads”), searchable by thread or source name, with a **Review** action per row. You can **Dismiss** rows from the feed — this clears the reminder only; the disclosed information stays available and you can still review it from each thread’s Updates page at any time. ## Reviewing an update [Section titled “Reviewing an update”](#reviewing-an-update) Each disclosed item is one row — a **Field**, **File**, **Identifier**, or **Certificate** — with a status: | Status | Meaning | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | **New** | The source disclosed something not on your thread yet. | | **Update** | The source changed something you already carry a copy of. | | **Needs review** | It can’t be applied automatically — for example a replaced file, or information not linked to anything on your thread yet. | | **Removed by source** | The source withdrew it from the disclosure; your local copy is untouched. | | **Changed locally** | Your copy has diverged from the source’s. | | **Skipped** / **Hidden locally** | You decided earlier; some decisions “can be brought back”. | | **In sync** | Your copy already matches. | Each row shows the comparison — “Source says” versus “This thread has” — and offers the matching action: * **Add to thread** — take a new item onto your thread. * **Update local copy** — apply the source’s change to your existing copy. * **Use source info** — resolve a **Needs review** item by taking the source’s version. * **Acknowledge** — for certificates: note it without adding a copy (including voided ones — “the voided file is on this thread — hide your copy, or acknowledge to keep it”). * **Skip** — decline it: “The source information stays visible; this thread just won’t take it in.” * **Hide local copy** — when the source removed or voided something you’d adopted: keeps the record in history but removes it from your thread’s active data by making the local copy private and archived. You can restore it later. If you already disclosed that item onward, DICE reminds you to also remove it from your own Fabric disclosure. Every decision can carry an optional note. Adopting applies the change to *your* copy — your thread stays yours; nothing changes on it without your action. **Review all** opens a batch review that applies every automatic **New**/**Update** row at once with a single **Add and update** confirmation; **Needs review** items still require individual decisions. Anyone who can see the thread can look at its updates, but “only an editor of this thread can apply them.” ## The disclosure record [Section titled “The disclosure record”](#the-disclosure-record) Every push is recorded. In the [Fabric explorer](/use/fabric/), select a link and open its **Timeline** tab to see the **Disclosure timeline**: each disclosure revision, who made it (the **Actor**), when the event cursor advanced, and any reason that was entered at push time. This gives both sides — source and recipient — the same durable record of what was disclosed and when. # Fabric: provenance and lineage > Trace where a thread came from and what was derived from it — across shipments, slices, and organizations. **Fabric** is DICE’s provenance layer. Every time a thread is derived from another — shipped to another organization or sliced into new threads — Fabric records the link. The Fabric explorer shows that history as a graph, so you can trace a thread upstream to its origins and downstream to everything made from it, even when other links in the chain belong to other organizations. A lineage chain in the Fabric explorer: every transfer and slice leaves a permanent link, so provenance survives across organizations — without exposing anything the owners didn't disclose. Note Fabric availability depends on your organization’s configuration — the lineage surface appears when Shipments or Slicing is enabled. If you don’t see it, it isn’t enabled for your organization (see the [FAQ](/use/faq/)). ## Open the explorer [Section titled “Open the explorer”](#open-the-explorer) On a thread’s detail page, the **Lineage** card summarizes provenance: what this thread was **Created from** and what was **Derived from this thread** (each row labeled **Shipment** or **Slice**). Click **View provenance** to open the full explorer — the **Provenance** page at `/app/threads//fabric`. A thread with no Fabric history shows “**No lineage yet**”: links appear here once it is transferred or sliced. ![The Fabric explorer showing lineage links from an alloy billet thread to the fan blades derived from it](/_astro/fabric-explorer.BCJby261_Rrfg8.webp) The explorer maps lineage left to right — here fan blades sliced from the billet they were machined from, with the inbound shipment link behind them. ## Reading the graph [Section titled “Reading the graph”](#reading-the-graph) The graph is a left-to-right lineage map — upstream sources on the left, downstream derivations on the right. Click **Show graph legend** for the in-app **Lineage legend**; here is what it covers. ### Nodes are threads [Section titled “Nodes are threads”](#nodes-are-threads) Each node is a thread. Its state tells you how much of it you can see: * **Your thread** — the thread you opened, or a thread your team owns. Opens its live data. * **Openable** — you have live access to this thread and can open its live view. * **Disclosed only** — no live access. Only the data disclosed through Fabric is visible. That last state is the important one: threads owned by other organizations appear in your graph, but you see only what their owner chose to disclose — typically the thread’s identity plus whichever assets were shared downstream. Each card shows a thumbnail — when you have live access to the thread — and a **Thread Version** selector that reports which version you are currently viewing: a green **Live** marker for live data, or a locked kind-and-date label such as **Transfer · Jul 6, 2026** when you are viewing a fixed version (see [Versions](#versions)). A card with more than one version also carries a count of its fixed versions, and the thread you opened keeps a highlighted border. An assembly card is marked **Assembly · *N* Parts**, and a card that is a part of an assembly shows a **Part of *⟨name⟩*** chip you can follow to the parent. ### Links are derivations [Section titled “Links are derivations”](#links-are-derivations) Edges between nodes are **Fabric Links**, and each has a kind: * **Transfer** — cross-organization derivation that changes ownership (a [shipment](/use/shipments/)). * **Slice** — same-team derivation; ownership of the source is unchanged (see [Slicing](/use/slicing/)). Use **Expand graph** for a full-screen view of large lineages — drag to pan, scroll to zoom, and click a node to inspect it. ## Inspecting a thread node [Section titled “Inspecting a thread node”](#inspecting-a-thread-node) Selecting a node opens a detail panel. For your own or openable threads it shows the live thread. For **Disclosed only** threads it shows the disclosed view: * **Data** — the disclosed data fields. * **Files** — disclosed images, media, and documents. * **Identifiers** — disclosed identifiers. Counts like “**3/7 disclosed**” tell you how much of the upstream thread you’re seeing. A notice reminds you: “You are viewing this upstream thread as disclosed through one Fabric Link, not its live data.” Items the source later pulled back are marked **Removed** or **Redacted**. ### Versions [Section titled “Versions”](#versions) A thread can exist as more than one **Thread Version**: its **Live** state — the current data, when you have live access — plus a fixed version for each point it was transferred, sliced, or had a disclosure pushed down its chain. The **Thread Version** selector lives on each node’s card. Open it to move between versions; options are labeled by kind and the date the version came into being, such as **Slice · Jul 6, 2026** or **Transfer · Jul 6, 2026**. A fixed version shows the thread exactly as it was captured at that moment, while **Live** always reflects the current data. What you see everywhere else — the detail panel included — follows the version you pick. ### Assembly [Section titled “Assembly”](#assembly) When the selected node is an [assembly](/use/assemblies/) — or a part of one — the detail panel adds an **Assembly** section listing its attached parts. Each part links to its own Fabric view, and a parent row takes you up to the assembly it is installed in. This section is version-faithful: with **Live** selected it shows the assembly’s current parts; with a fixed version selected it shows the parts exactly as they were captured in that version. Parts that were not included in a fixed version simply do not appear. ## Inspecting a link [Section titled “Inspecting a link”](#inspecting-a-link) Selecting an edge opens the **Link inspector**, with three tabs: * **Context** — the link’s **Source** and **Target** threads and its **Disclosures**: which fields, files, identifiers, and certificates flow through this link. A link with no disclosed assets is marked “structure-only — no assets are disclosed through it.” Items show **Included**, **Redacted**, or **Removed**. * **Events** — the activity disclosed through this link (see below). * **Timeline** — the **Disclosure timeline**: every disclosure revision on this link, who made it, and when the event cursor advanced. ## The event feed and what you don’t see [Section titled “The event feed and what you don’t see”](#the-event-feed-and-what-you-dont-see) Disclosed threads and links carry an event feed — a projected slice of the source’s Transaction History. Two things shape what appears: * **The event cursor.** Sources control how far the feed extends. A link marked **Locked** has “disclosed events fixed at” a point in time; **Open** means no event cursor has been fixed for this link yet. Sources can advance the cursor when they [push a disclosure](/use/disclosures/). * **Withheld items are omitted.** Events about assets the source never disclosed to you simply do not appear — there is no placeholder or count. An event you *can* see may carry a **Redacted** marker when the source disclosed that something happened but withheld the details. So an upstream feed that looks short isn’t broken; you’re seeing exactly what was disclosed, and nothing about what wasn’t. ## Acting from the explorer [Section titled “Acting from the explorer”](#acting-from-the-explorer) The **Provenance actions** menu offers, depending on your access: * **Review updates** — review what upstream sources have disclosed to this thread (see [Disclosures and updates](/use/disclosures/)). * **Push disclosure** — select one of your own source threads in the graph, then push more disclosure down its fabric chain. * **Slice this thread** — derive new threads from this one (see [Slicing](/use/slicing/)). # FAQ and troubleshooting > Answers to common DICE questions — sign-in issues, permissions, scanning, shipments, updates, module availability, and language settings. Answers below reflect current DICE behavior. If your problem isn’t covered, email [](mailto:support@dustidentity.com). ### I’m stuck in a sign-in loop, or signed in as the wrong account [Section titled “I’m stuck in a sign-in loop, or signed in as the wrong account”](#im-stuck-in-a-sign-in-loop-or-signed-in-as-the-wrong-account) DICE signs you in through your DUST account. If the browser is holding a session for the wrong account (for example, a personal account from the same identity provider), sign out **fully** first: 1. In DICE, open the user menu (top right) and choose **Sign out**. This ends your DICE session *and* revokes your DUST account sessions — it’s a full sign-out, not just a local one. 2. Sign in again. When your identity provider shows an account picker, choose the account your organization invited (check the email address carefully — invitations are tied to a specific address). If sign-in keeps bouncing back to the login page, clear cookies for the DICE and DUST account domains and try again in a fresh browser window. Still stuck? Contact [](mailto:support@dustidentity.com) with the email address you’re signing in with. ### I signed in but don’t see my organization [Section titled “I signed in but don’t see my organization”](#i-signed-in-but-dont-see-my-organization) After sign-in DICE asks you to select your organization and Team. If you see **“No selectable org/team contexts are available for this account yet.”**, your account exists but hasn’t been added to an organization and Team yet: * **You:** contact your organization’s administrator and ask to be invited and placed on a Team. Make sure they invite the exact email address you sign in with. * **Admins:** the invitation and Team assignment flow is described in the [Administrator guide](/use/admin-guide/). Note that invitations expire after 48 hours. Once you’ve been added, sign in again (or use **Sign out** and return) and the organization will appear in the selector. ### The scanner says camera access is denied [Section titled “The scanner says camera access is denied”](#the-scanner-says-camera-access-is-denied) The scanner needs the browser’s camera permission. If you dismissed or denied the prompt, DICE shows **“Browser blocked camera access”**: “Your web browser (Chrome, Edge, Firefox, or Safari) is preventing this page from using the camera. Open the site permissions in your browser settings and allow camera access for this page, then reload.” To re-trigger it: 1. Click the **camera/lock icon in the browser’s address bar** and set camera access to allowed for this site. 2. If there’s no icon, go through the browser’s settings — typically **Settings → Privacy → Site settings → Camera** — and allow this site. 3. Reload the page and open the scanner again. Other camera errors are usually environmental: “Camera is in use by another application” (close other apps using the camera) or “No cameras found on this device.” ### Why does DICE ask for my location? [Section titled “Why does DICE ask for my location?”](#why-does-dice-ask-for-my-location) On first use DICE runs a short device setup (“Set up your device”) that requests **camera and location** permissions. Location is optional but recommended: “DICE attaches your location to scans and writes for full traceability. This helps meet chain-of-custody requirements and proves where actions happened.” You can **skip** either step during setup and enable the permission later in your browser’s site settings, the same way as the camera above. ### What’s the difference between Identify and Verify? [Section titled “What’s the difference between Identify and Verify?”](#whats-the-difference-between-identify-and-verify) Both are scanner operations, switchable in the scan screen: * **Identify** — you don’t know which Thread you’re holding. Scanning searches your indexed Identifiers and *finds* the matching Threads. * **Verify** — you already have a specific Thread open (or queued) and want to *confirm* it’s the right physical item. Scanning matches the capture against that Thread’s bound Identifier and reports pass/fail. One nuance for DUST Identifiers: some organizations use **verify-only** DUST. In that case “Identify is unavailable — this organization only supports verify-only DUST. Non-DUST tags still identify normally.” See [Identifiers and scanning](/use/identifiers-and-scanning/). ### It says this thread is already in a shipment [Section titled “It says this thread is already in a shipment”](#it-says-this-thread-is-already-in-a-shipment) A Thread can be in **only one active shipment at a time**. When you add Threads to a shipment, “already-shipped threads are hidden; threads already in another active shipment are shown but can’t be selected” — each blocked row is marked **In “\”**, which links to the shipment holding it. To move the Thread, open that shipment and remove the Thread from its manifest (if it’s still a draft), or wait for the shipment to complete. Threads in a sent shipment are locked (“This thread is locked in a shipment.”) until the shipment resolves. See [Shipments](/use/shipments/). ### A shipment failed to process — what now? [Section titled “A shipment failed to process — what now?”](#a-shipment-failed-to-process--what-now) If an accepted shipment fails, DICE shows **“Processing failed”**: “This accepted shipment didn’t finish processing. Nothing was created on the receiving team. Retry to run it again, or abandon it to release its threads.” The failure rolled back cleanly — nothing was shipped. You have two options: * **Retry** — runs processing again on the same shipment. * **Abandon** — gives up on this attempt (a reason is required and recorded in the shipment’s history). “Its threads are released and can be shipped again”, and you can then start a **new shipment from this manifest** so you don’t rebuild the list from scratch. See [Shipments](/use/shipments/) for the full lifecycle. ### I got an update from an upstream source — what does adopting it do? [Section titled “I got an update from an upstream source — what does adopting it do?”](#i-got-an-update-from-an-upstream-source--what-does-adopting-it-do) When a source organization discloses new or changed information for a Thread you received, it appears on the **Updates** page and on the Thread itself: “New information disclosed by sources of your threads. Review a thread to choose what to bring in.” Nothing is applied automatically — you review each item and choose an action: **Add to thread** (bring in something new), **Update local copy** (apply the source’s newer value to data you already adopted), **Use source info**, or **Acknowledge** (for certificates). Adopting copies the disclosed value into *your* Thread; skipped items can be brought back later, and items you’ve edited locally are flagged (**Changed locally** / **Needs review**) rather than silently overwritten. See [Disclosures](/use/disclosures/). ### Why don’t I see feature X? [Section titled “Why don’t I see feature X?”](#why-dont-i-see-feature-x) Two common reasons: 1. **The module isn’t enabled for your organization.** Capabilities such as Assemblies, Categories, Shipments, Certificates, Slicing, Verification, Tamper Analysis, Public Pages, Connections, and Sharing are enabled per organization by DUST Identity. When a module is off, its navigation entries are **hidden entirely** — there’s no greyed-out item to click. Your admin can request changes via [](mailto:support@dustidentity.com) (see the [Administrator guide](/use/admin-guide/)). 2. **Your role.** Some areas are role-gated — for example **Connections** only appears in the sidebar for admins of the active Team. Others are visible to everyone but read-only without the right grant: every team member can open **Public Page Designs**, but only a **Publisher** (or a Team admin) can create, publish, or roll anything out there. See [Public Pages](/use/public-pages/). If a colleague sees a feature you don’t, compare organization, Team, and role first. See also [Navigating DICE](/use/navigation/). ### How do I switch the interface language? [Section titled “How do I switch the interface language?”](#how-do-i-switch-the-interface-language) Open the user menu (top right), choose **Language**, and pick **English** or **中文**. The choice applies immediately and is remembered per browser. ### Who do I contact for help? [Section titled “Who do I contact for help?”](#who-do-i-contact-for-help) Email [](mailto:support@dustidentity.com). Include your organization name, the page you were on, and (for scanning or shipment issues) roughly when the problem happened — that makes it much faster to trace. Tip For orientation and first steps, start with [Getting started](/use/getting-started/). # Folders > Organize threads into a hierarchy of folders and subfolders, move them safely, and let DICE remember your working folder. Folders are where threads live. Every thread has exactly one home folder, and folders nest to form a hierarchy. As the empty state puts it: “Folders help you organize threads into meaningful groups. Create a folder to start curating your workspace and make bulk operations easier.” Folders are single-home organization. For additive, cross-cutting grouping — where one thread belongs to several groups at once — use [Categories](/use/categories/) instead. ## Browse the folder hierarchy [Section titled “Browse the folder hierarchy”](#browse-the-folder-hierarchy) Open **Folders** in the sidebar. The page has two parts: * A **Folders** panel with the tree. It is split into **Our folders** (owned by your team) and **Shared with us** (folders other teams shared with your team). Expand and collapse branches, or type in **Search folders…** to filter. Shared folders are badged with your access: “Shared with you — you can edit” or “Shared with you — view only”; folders you have shared out show “Shared with others”. * The selected folder’s contents: a thread count, the list of threads inside, and a **Create Thread** button that creates a new thread directly into this folder. Breadcrumbs above the contents show the selected folder’s full path; on long paths, click **Show full path** to expand the collapsed middle. On small screens the tree is behind a **Select Folder** button that opens a **Browse Folders** sheet. ![The Folders page with the folder tree on the left and the selected folder's threads on the right](/_astro/folders.BtnemAsT_ZmlzVp.webp) The Folders page: the tree on the left, the selected folder's threads on the right. ## Create folders and subfolders [Section titled “Create folders and subfolders”](#create-folders-and-subfolders) 1. Click **New Folder** in the Folders panel to open the **Create a Folder** dialog (“Create a top-level folder to organize your threads.”). Enter a name — “Use a short, specific name so the folder is easy to scan later” — and click **Create**. 2. To nest, open a folder’s actions menu in the tree and choose **Add Sub-Folder**. The dialog becomes **Create a Sub-Folder** and confirms the location: “Create a folder inside *path*.” You can also create folders inline from any destination folder picker (**New Folder** / **New Subfolder**), for example while creating a thread. “Top-level folders are created in your active team’s workspace.” ## Folder actions [Section titled “Folder actions”](#folder-actions) Each folder in the tree has an actions menu: * **Share** and **Access** — share the folder with other teams and review who has access; see [Sharing and Access](/use/sharing-and-access/). * **Rename** — “Update the folder name everywhere it appears in your workspace.” * **Add Sub-Folder** * **Move to Folder** and **Move to Root** (below) * **Delete** — the **Delete Folder** dialog warns “This action cannot be undone.” and clarifies “This will permanently delete the folder. Threads inside will not be deleted.” ## Move a folder [Section titled “Move a folder”](#move-a-folder) Choose **Move to Folder** to open the **Move to folder** dialog and “Choose a destination” for the folder. The dialog: * Scopes the tree to **Our team folders** or, for folders owned by a connected team, **Connected team folders** — “A folder owned by a connected team can only be re-parented within that same team’s folders.” * Marks the **Current folder** and summarizes eligibility, e.g. “3 of 8 folders can accept this folder.” * Moves on **Move here**; a “Folder moved” confirmation appears. **Move to Root** lifts a folder to the top level. If your only access to that folder comes through the shared parent you are removing it from, DICE warns you first: * **You’ll lose access** — moving it out of the shared folder means “you’ll lose access to it completely.” Confirm with **Move and lose access**, or **Cancel**. * **You’ll lose edit access** — your access would drop “from edit to view-only.” Confirm with **Move and switch to view**. * Otherwise the dialog is a simple **Move to root?** confirmation. ## Add threads to a folder [Section titled “Add threads to a folder”](#add-threads-to-a-folder) * **Create in place** — the **Create Thread** button inside a folder, or choosing that folder in the **Folder** picker when creating a thread. * **Move existing threads** — select one or more threads and choose **Move to Folder**. The **Move to folder** dialog works like the folder move: pick a scope, see how many folders “can accept” the selection, and click **Move here**. If some of the selected threads already live in the destination, DICE notes “Some selected threads already live here; the rest will join them.” A few rules the dialog enforces: * Threads can only land in folders owned by the same team that owns the threads. A mixed selection shows “Selected threads span multiple teams” — move them in batches grouped by owner. * In folder pickers, hover hints explain eligibility: “You can edit — items can be moved here”, “View only — items can’t be moved here”, and “Shared with other teams — items moved here are visible to them”. ## The working folder (“Last used”) [Section titled “The working folder (“Last used”)”](#the-working-folder-last-used) DICE remembers the folder you last worked in — per user, per organization and team — and uses it as the default destination everywhere a folder picker appears (creating a thread, importing, slicing, moving). The remembered folder shows a **Last used** badge in pickers. The working folder updates when you browse a folder on the Folders page or complete a destination action — creating a thread, committing an import, creating a slice, or finishing a move. It is only a default: DICE re-checks that the folder is still available and editable before pre-filling it, and you can always pick a different destination. Tip The Home page’s “Pick up where you left off” section shows your current working folder with an **Open folder** shortcut. See [Navigating DICE](/use/navigation/). # Getting started with DICE > Sign in, pick your organization and team, create your first folder and thread, and bind your first DUST identifier. DICE keeps a digital record — a **Thread** — for every physical item you care about. This page walks you through your first session: signing in, choosing where you work, creating a folder and a thread, and binding an identifier so the physical item and its digital record are linked. (If you want the mental model before the walkthrough, start with [Core concepts](/use/core-concepts/).) ## Sign in [Section titled “Sign in”](#sign-in) DICE uses single sign-on: your account lives with your organization’s identity provider, and DICE never asks you to manage a separate password. 1. Open your DICE URL in a browser. The landing page greets you with **Welcome back**. 2. Click **Sign In**. You are redirected to your organization’s sign-in service; authenticate there as usual. 3. After signing in you land on the app Home page. On your next visit, an active session shows a **Go to App** button instead of **Sign In**. Don’t have an account yet? The landing page includes a **Contact Sales** link — access to DICE is provisioned by your organization, so there is no self-service sign-up. Tip The landing page also has language and theme toggles in the top-right corner, so you can switch to your preferred language before you even sign in. See [Navigating DICE](/use/navigation/) for switching later from inside the app. ![The DICE Home page after signing in, showing pending actions, recent threads, and the activity feed](/_astro/home.o2t-bngl_1Dx9vm.webp) After signing in you land on Home: pending actions for your team, your recent threads, and the latest activity. ## Choose your organization and team [Section titled “Choose your organization and team”](#choose-your-organization-and-team) Everything you do in DICE happens inside a context: an **organization** and a **team** within it. Threads, folders, and permissions all belong to a team. If your account has access to more than one context and none is selected yet, DICE shows the **Select your organization and team** page after sign-in: 1. Pick an organization from the **Organization** dropdown. 2. Pick a team — the page notes “Note, you can change this at any time.” 3. Click **Continue**. ### Switching context later [Section titled “Switching context later”](#switching-context-later) The context switcher sits in the app’s top bar and always shows your current organization and team. Click it to open a panel with: * **Current Context** — your active organization and team. A shield icon marks contexts where you are an admin. * **Switch Organization** — a dropdown of every organization you belong to. Choosing one also selects a default team in it. * **Switch Team** — the teams available to you in the current organization. Switching context changes what you see everywhere in the app: thread lists, folders, and activity all reflect the active team. ## Create your first folder [Section titled “Create your first folder”](#create-your-first-folder) Folders give threads a home. Every thread lives in exactly one folder, so create one before creating threads. 1. Open **Folders** in the sidebar. 2. If your team has no folders yet, the page invites you to “Create your first folder”. Click **New Folder**. 3. In the **Create a Folder** dialog, enter a name — the hint suggests “Use a short, specific name so the folder is easy to scan later” — and click **Create**. You can nest folders and reorganize them later; see [Folders](/use/folders/). ## Create your first thread [Section titled “Create your first thread”](#create-your-first-thread) 1. Click **New Thread** in the sidebar. The **Create a New Thread** page opens. 2. Under **Details**, enter a **Name** (required) and optionally a **Description**. 3. Choose a **Folder** (required). DICE pre-fills your most recently used folder when it can — look for the **Last used** badge in the folder picker. 4. Optionally pick a **Template**. Templates pre-populate the form with a **Thread Fields** section — a reusable set of field definitions your team manages under Threads > Templates. Without a template you get a **Custom Fields** section where you can add fields freely. Both are covered in [Threads](/use/threads/). 5. Fill in any fields, then click **Create Thread**. A “Thread created” confirmation appears and DICE opens the new thread’s detail page. ![The New Thread form with a name and description filled in and a folder selected](/_astro/thread-create.CqAhXCmY_1LMzdk.webp) Creating a thread: name it, pick its folder, and DICE opens the new record. ## Bind your first DUST identifier [Section titled “Bind your first DUST identifier”](#bind-your-first-dust-identifier) Binding attaches an identifier — a DUST tag, QR code, barcode, Data Matrix, or NFC tag — to a thread, so scanning the physical item finds its record. 1. On the thread’s detail page, find the **Identifiers** card and click **Bind**. The scanner opens in bind mode, described as “Scan identifier to bind it to this thread.” 2. Allow camera access when the browser asks. If the browser has blocked it, the scanner explains how to re-enable it (“Click the camera/lock icon in the browser’s address bar and choose **Allow** for camera”, then reload). 3. Choose your **Scanner mode**: **Camera** to scan visually, or **Manual** to paste, type, or use a HID/Bluetooth scanner (“Manual Entry — Paste, type, or use a HID/Bluetooth scanner.”). 4. For camera scanning, line the code up in the on-screen reticle. **Scanner Settings** let you choose the camera device and adjust the reticle (color, style, blend, fullscreen). You can also tap the spacebar to scan. 5. For QR codes, barcodes, and similar formats, DICE shows a confirmation dialog — for example **Confirm QR Bind** — with the **Detected Value**, an optional **Tag Description**, and a confirm button, so you can “Review the scanned value before binding to this thread.” 6. On success you’ll see **Successfully Bound!** and the identifier appears on the thread. Note DUST tag binding is plan-dependent. When binding a DUST tag you also choose an **Indexing Mode**: **Verify + Identify** (“This DUST can be verified against a thread and identified from any scan”) or **Cannot be identified** (“This DUST can only be verified against the thread it was bound to”). If DUST operations are not on your organization’s plan, the scanner says so and other identifier types (QR, barcode, Data Matrix, NFC) still work normally. See [Identifiers and Scanning](/use/identifiers-and-scanning/). ## Where to go next [Section titled “Where to go next”](#where-to-go-next) [Navigating DICE](/use/navigation/)The sidebar, Home page, context switcher, and user menu. [Threads](/use/threads/)Fields, files, templates, archiving, and the thread detail page. [Identifiers and Scanning](/use/identifiers-and-scanning/)Bind, verify, and identify with DUST and other identifier types. [Folders](/use/folders/)Organize threads into a folder hierarchy. [Sharing and Access](/use/sharing-and-access/)Share threads and folders with other teams. [FAQ](/use/faq/)Answers to common questions, including feature availability. # Identifiers and scanning > Bind, verify, and identify Threads with DUST, QR, barcode, data matrix, and NFC identifiers using the DICE scanner. An identifier is a physical marker bound to a [Thread](/use/threads/) so the physical item and its digital record can be matched to each other. DICE supports five identifier types: | Type | What it is | | ----------- | ---------------------------------------------------- | | DUST tag | A DUST Identity tag read optically by a DUST scanner | | QR code | Any QR symbol | | Barcode | Linear barcodes | | Data matrix | 2D data matrix symbols | | NFC | NFC tags, read by their hex ID | All scan workflows start from the **Scan** page in the navigation, or from a Thread’s identifier list. ![The Identify page on the Scan screen, with DUST, Camera, and Manual modes and a team scan scope](/_astro/scan.bFgtkC4K_Z1nvWQB.webp) The Scan page's Identify operation: pick a scanner mode (DUST, Camera, or Manual) and the teams to search. ## The three scan operations [Section titled “The three scan operations”](#the-three-scan-operations) * **Bind** attaches a scanned identifier to a Thread (“Scan an identifier to bind it to this Thread”). Once bound, the identifier appears in the Thread’s identifier list. * **Verify** confirms a specific identifier already bound to a Thread: you pick the identifier on the Thread, scan the physical item, and DICE reports whether it matches (“Identifier matches expected value”) or fails. * **Identify** goes the other direction: scan any identifier and DICE finds the Thread(s) it is bound to (“Find Threads by scanning their identifiers”). Use Identify when you have an item in hand and want to know what it is; use Verify when you already know which Thread it should be and want proof. If a scanned identifier is bound to more than one Thread, a **Multiple Matches Found** dialog lists every match — filterable by Thread name, description, or organization — so you can open the right one. ## Scanner modes [Section titled “Scanner modes”](#scanner-modes) A segmented switcher at the top of the scanner selects the input method: * **DUST** — the DUST camera scanner. Point the reticle at the tag and press **Scan** (or tap spacebar). * **Camera** — the device camera reads QR codes, barcodes, and data matrix symbols. * **Manual** — **Manual Entry**: “Paste, type, or use a HID/Bluetooth scanner.” Choose the identifier type (**QR Code**, **Barcode**, **Data Matrix**, **NFC**), enter the value, and press Enter to submit. Standalone timestamp prefixes and suffixes are removed automatically, so wedge-scanner input works as-is. ## DUST indexing modes [Section titled “DUST indexing modes”](#dust-indexing-modes) When binding a DUST tag you choose an **Indexing Mode**: * **Identifiable** (“Verify + Identify”) — the DUST can be verified against its Thread and also matched from any Identify scan. * **Verify-only** (“Cannot be identified”) — the DUST can only be verified against the Thread it was bound to; Identify scans will not return it. The scanner shows a help popover (“About DUST indexing modes”) explaining this in place: only Identifiable DUST can be matched from an Identify scan, while both indexing modes can be verified. Note Which DUST capabilities you see (bind, verify, identify, and each indexing mode) depends on your organization’s plan. If an operation is gated you’ll see a notice such as “DUST identify is not on your plan” — other identifier types (QR, barcode, data matrix, NFC) work normally. See [FAQ](/use/faq/). ## Scan scope for Identify [Section titled “Scan scope for Identify”](#scan-scope-for-identify) The Identify screen includes a **Scan scope** panel: “Pick which teams to match against when identifying a thread.” By default the scope covers **every team available to you** — your own Teams and Teams connected to yours — and the panel shows a summary like “Searching in all N teams”. You can narrow it to the **Current** Team, all Teams in my org, or any custom set; your selection is remembered per Team. Identify results can therefore span organizations, which is called out in the match dialog (“N matches found … across N organizations”). When an Identify scan does not find a match, the message tells you why and what to do next: * **Scan not clear enough** — the capture was rejected for focus or quality; hold steady and scan again. * **No match found** — every team in your scan scope was searched and nothing matched. The identifier may be unbound, or belong to a team outside your scan scope; use **Adjust scan scope** to review where you are searching. * **Search incomplete** — some teams could not be searched, so the result is not a definitive “not found”; use **Try again**. * **Connection problem** — the scan never reached DICE; check your connection and try again. ## Camera permissions [Section titled “Camera permissions”](#camera-permissions) The Camera and DUST modes need browser camera access. If the browser blocks it, the scanner shows **Browser blocked camera access** with recovery steps: 1. Click the camera/lock icon in the browser’s address bar and choose **Allow** for camera. 2. Or open browser **Settings → Privacy → Site settings → Camera** and allow this page. 3. Reload the page after granting access. The **Camera settings** popover (“Choose which camera to use for scanning.”) lets you pick a specific device when more than one camera is available. ## Reticle and scanner settings [Section titled “Reticle and scanner settings”](#reticle-and-scanner-settings) The DUST scanner overlays a reticle to help you center the tag. Open **Scanner Settings** to adjust it: * **Device** — select which connected DUST scanner to use (**Refresh connected devices** rescans). * **Show Reticle** — toggle the overlay. * **Reticle Color** and **Reticle Style** — styles are **Default**, **Thick**, **Crosshairs**, **Duplex**, and **None**. * **Blend Reticle** — blends the reticle into the camera image. * **Fullscreen** — expands the scanner view. If the DUST scanner isn’t detected you’ll see **Scanner not connected** with troubleshooting tips (reseat the USB cable, try a different port, avoid unpowered hubs). ## Binding workflow details [Section titled “Binding workflow details”](#binding-workflow-details) When binding, a context card shows the Thread you’re binding to, its **Existing Identifiers**, and a **Tag Description** field (e.g. “Front label”) so you can note where the identifier sits on the item. Two conveniences: * Camera/manual binds show a **Confirm** step (“Review the scanned value before binding to this Thread”) with the **Detected Value** before anything is written. * **Stay after bind** — a toggle on the bind screen that keeps the current item selected after a successful bind (“Keep the current item selected.”), useful when adding several identifiers to one Thread. ## Verifying and unbinding from a Thread [Section titled “Verifying and unbinding from a Thread”](#verifying-and-unbinding-from-a-thread) On a Thread’s identifier list, open an identifier to see its details and verification state (**Verified** / **Not Verified**). From there: * **Verify** starts a scan against that specific identifier. A failed DUST verify reports: “This DUST doesn’t match the selected tag.” * **Unbind** removes the identifier from the Thread after a confirmation (**Unbind Identifier?** — “This will remove the identifier from this Thread. This action cannot be undone.”). Every bind, verify, identify, and unbind is recorded in the Thread’s history — see [Activity and transaction history](/use/activity/). ## Voiding and archiving an identifier [Section titled “Voiding and archiving an identifier”](#voiding-and-archiving-an-identifier) Over an item’s life it can carry several identifiers — dust gets destroyed, re-applied, or the marked material is cut away. **Void** and **Archive** both mark an identifier without removing it, and they do opposite things to what you see. | | What it means | How it looks | | ----------- | ---------------------------------------------------------- | ------------------------------------------------------- | | **Void** | This identifier is no longer the live marking for the item | Stays in the identifier list, in red, marked **Voided** | | **Archive** | Hide this identifier from the default view | Moves into a collapsed **Archived identifiers** section | Both are set from an identifier’s detail dialog, and both are reversible — **Remove void** and **Restore** put an identifier back. They are independent: an identifier can be voided, archived, or both. Voiding is a label and nothing more: * The identifier stays bound to the Thread with its full history intact. * It can still be identified, verified, and unbound exactly as before. * Nothing about the underlying DUST record changes. * If a voided identifier turns up in an Identify result, DICE flags it — “This identifier was marked voided. Check whether it should still be in use.” — but never blocks the scan. Voiding and un-voiding are recorded in the Thread’s history, and a void travels with the identifier when a Thread moves through a [shipment](/use/shipments/). Note Voiding an identifier is not the same as voiding a [Certificate](/use/certificates/). A certificate void is permanent; an identifier void can be removed. ## Scanning with DUST Go [Section titled “Scanning with DUST Go”](#scanning-with-dust-go) The mobile counterpart to the browser scanner lives at **Scan → Dust Go**. DUST Go binds, verifies, and identifies “directly from the mobile hardware scanner” and reads “DUST, QR, barcode, Data Matrix, and NFC in one flow” — press the on-screen prompt to trigger the next scan. If DUST isn’t enabled for your organization, DUST Go can still read the non-DUST identifier types. ### What the scanner shows you [Section titled “What the scanner shows you”](#what-the-scanner-shows-you) Recent DUST Go builds show the scan’s context in the viewfinder and answer each scan there, so a run of scans needs no trips back to DICE: * **What the next scan is for** — the Thread being worked, the identifier being verified, and for a batch, the queue with the current position marked. * **The result of each scan** — bound, verified, no match, not found, or a connection problem, shown over the viewfinder as soon as DICE answers. The same outcome still appears in DICE when you return to it. * **Name an identifier while you hold it** — on a bind, the suggested identifier name is editable in the scanner. What you type there is used for that bind, in place of the description entered in DICE. * **Move around a batch queue** — pick another Thread from the queue in the scanner. Stepping forward off a Thread you haven’t finished marks it **skipped**: it stays in the queue and still counts as outstanding, so the batch is not complete until you come back to it. Older DUST Go builds are unaffected — they scan exactly as before. [Integrate with DUST Go](/integrate/dust-go/)How the DUST Go mobile app and its hardware scanning work. ## Batch scanning [Section titled “Batch scanning”](#batch-scanning) **Batch Scan** processes many Threads in one sitting — either binding an identifier to each Thread in a queue, or verifying a whole queue of Threads. Reach it from the Scan page, or select multiple Threads in a folder and choose **Batch Scan** from the selection bar. 1. **Add Threads** — build the queue by picking Threads from a folder (**Browse folders**) or by scanning to identify them (**Scan to add**). The queue persists while you switch between the two. 2. **Scan to bind** or **Scan to verify** — for bind, “each scan binds the next Thread in the queue”; for verify, “each scan verifies the active Thread”. 3. **Track progress** — the queue shows live status per Thread, a ready count (“N ready / M queued”), and a completion state (“All Threads bound” / “All verifiable Threads verified”). Batch behavior worth knowing: * **Verify needs identifiers.** Queued Threads with no identifiers are kept in the queue but skipped until an identifier is added (“N Threads need identifiers”). * **Overall Verify Mode** — **Any Match** accepts any matching identifier on the Thread; **All Tags** requires each candidate identifier to be verified. A per-Thread **Thread Verify Mode** override is available under **Show advanced**. * **Failures stay in the queue.** A failed scan marks the Thread and you can simply scan it again; **Reset States** (under **Queue actions**) clears all statuses to rerun the batch, and **Clear Queue** empties it. * **Skipped Threads stay in the queue too.** A Thread you stepped past in the DUST Go scanner without finishing is marked skipped and remains outstanding — the batch is not complete until it is done or removed. * **Stay after bind** works here too, and each bind can carry a **New Tag Description** (e.g. “Inside logo”). Batch scanning also has a DUST Go variant (**Batch Scan** from within DUST Go) that adds identified Threads straight into the batch queue. ## Related pages [Section titled “Related pages”](#related-pages) [Threads](/use/threads/)The records identifiers are bound to. [Relationships](/use/relationships/)Scan an identifier to pick a Thread when linking. [Activity](/use/activity/)Where scan events are recorded. # Importing threads from CSV > Bulk-create Threads from a CSV file, with automatic template inference, field mapping, and a destination folder. The CSV importer turns a spreadsheet into Threads — one Thread per row. It lives at **Threads → Import Threads** and walks you through choosing a [template](/use/threads/), mapping columns to fields, picking a destination [folder](/use/folders/), and running the import with live progress. ## CSV format [Section titled “CSV format”](#csv-format) Prepare a header row, one Thread per row, and keep a column for the Thread name. The sidebar’s **CSV Format** panel suggests example columns (**Name**, **Description**, **Status**, **Owner**), and **Download Example CSV** gives you a starting file. Only `.csv` files are accepted (“CSV files only”), and “this importer expects each row in the CSV file represents a single thread.” Two more format tips: * One column supplies the Thread name, and it must have a value in every row you want imported. DICE suggests a **Name**/**Title** column when it finds one, and otherwise picks the first column. * Column types (text, date, number) are detected automatically from the data when generating a template, so keep each column’s values consistent. ## Choose an import mode [Section titled “Choose an import mode”](#choose-an-import-mode) The **Import mode** section offers two paths: * **Create a new template and import** — “Upload a CSV, auto-generate a template, review, then import.” Best for a first-time import: DICE infers a template from your columns. * **Import into an existing template** — “Choose a template first, then map fields and import threads.” Available once you have at least one template; **Download Sample** produces a CSV matching the selected template’s fields. ## Import with a new template [Section titled “Import with a new template”](#import-with-a-new-template) 1. **Upload CSV** — click to upload or drag and drop. The file is parsed in place and the row count appears (“N rows ready to map”). 2. **Review the Template Builder** — the inferred template is shown with a **Template Name** and **Template Description** pre-filled from the file name, plus the detected **Data Fields**. 3. Under **Thread name & description**, confirm the **Name column** and optional **Description column**. Those columns are listed in **Data Fields** as **Thread name** / **Thread description** and marked **Not created as a field**, because each Thread already carries them. 4. Click **Create Template**. The template is saved to your Team and you move on to destination and import (“Template created. Continue to destination and import.”). Because the template was generated from your CSV, column mapping is applied automatically (“This template was generated from the uploaded CSV, so field mapping is applied automatically.”). The **Automatic Column Mapping** panel restates the name and description sources; use **Back to Template** to change them. ## Import into an existing template [Section titled “Import into an existing template”](#import-into-an-existing-template) 1. Under **Select Template**, pick the template that should receive the imported rows. 2. **Upload CSV** — “Upload a CSV file to start mapping and import.” The panel shows your **CSV Rows** next to the selected template’s **Template Fields**. 3. Click **Continue to Field Mapping**. ## Destination and field mapping [Section titled “Destination and field mapping”](#destination-and-field-mapping) The **Destination & Field Mapping** step is where you control what gets created: * **Folder** — the destination folder for every imported Thread. The picker pre-fills your most recently used folder; you must select one before importing. * **Field Mapping** — “Map each template field to the CSV column that should populate it.” Suggested mappings are applied by column-name match; fields left as **No column** are simply not populated. (Skipped when the template was auto-generated from the CSV.) * **Thread name & description** — the CSV column each Thread takes its **name** from (required) and, optionally, its **description**. These are properties of the Thread, so the columns are not stored as field data. * **Also store these columns as fields** — off by default. Turn it on to keep the name and description values in each Thread’s field data as well. A mapped field that is skipped says so under its name, including a **Required** one — the Thread’s own name and description carry the value instead. Below the mapping, the **Imported Threads Preview** shows the first rows as they will import, a readiness count (“N rows ready to import”), and flags rows that would fail (“N rows currently failing”) — typically rows missing a required value. Fix the CSV or the mapping until the rows you care about show **Ready**. Each preview row reads like the Thread page it becomes: the name, its template, its description, then the organization, team and folder it will belong to. That last line is worth a glance before you import — a folder shared with you belongs to another team, and Threads created in it belong to that team, not yours. ## Run the import [Section titled “Run the import”](#run-the-import) Click **Import N Threads**. The import runs in batches with a live **Import Status** (“Processing X of Y rows. Batch A of B.”), then reports a summary: “Imported N threads.” and, if applicable, “N rows failed.” Note The CSV import is row-by-row, not all-or-nothing: rows that fail are reported in the summary while the successful rows are still created. If everything succeeds you’re taken straight to the destination folder. A successful import also records an **Imported Thread** activity group per Thread in the [activity feed](/use/activity/), and the created template remains available for future imports. ## Importing an assembly package [Section titled “Importing an assembly package”](#importing-an-assembly-package) Separately from CSV import, DICE can import an **Assembly Import Package** — a JSON file describing Threads, assemblies, part attachments, and relationship links in one structure. It lives at **Assemblies → Import assembly package**. Note Assembly package import availability depends on your organization’s configuration — if you don’t see it in navigation it isn’t enabled (see [FAQ](/use/faq/)). 1. Paste the package JSON or **Load file**. 2. Click **Validate package**. The package is dry-run into an **Import plan**: counts of **Threads**, **Assemblies**, **Non-assembly Threads**, **Thread attachments**, and **Thread links** to be created, plus any **Blocking issues** and **Warnings**. Referenced folders, categories, and relationships are matched against what already exists and marked **reuse existing**, **create new**, or flagged as ambiguous. 3. When the plan reads “This package is ready to import.”, click **Commit import**. Unlike CSV import, the assembly package import is transactional: “nothing is written until you commit, and the import is all-or-nothing.” Resolve any blocking issues before committing. See [Assemblies](/use/assemblies/) for what assemblies and parts are. ## Related pages [Section titled “Related pages”](#related-pages) [Threads](/use/threads/)Templates and fields that imports populate. [Folders](/use/folders/)Where imported Threads land. [Assemblies](/use/assemblies/)Assemblies, parts, and positions. # Navigating DICE > The sidebar, Home page, context switcher, user menu, and other ways to move around the DICE web app. DICE is organized around a persistent left sidebar, a top bar with the context switcher and user menu, and a Home page that gathers what needs your attention. ## Sidebar [Section titled “Sidebar”](#sidebar) The sidebar is grouped into sections. Some entries only appear when the corresponding module is enabled for your organization, and one section only appears for team admins. Note Availability of the flagged entries below depends on your organization’s configuration — if you don’t see an entry in your navigation, that module isn’t enabled for your organization (see the [FAQ](/use/faq/)). ### General [Section titled “General”](#general) | Entry | Where it goes | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | **Home** | The dashboard described below. | | **Scan** | The scanner, opened in identify mode — find threads by scanning their identifiers. See [Identifiers and Scanning](/use/identifiers-and-scanning/). | | **Activity** | The event log for your team. See [Activity](/use/activity/). | | **New Thread** | The thread creation page. See [Threads](/use/threads/). | ### Threads [Section titled “Threads”](#threads) | Entry | Where it goes | Availability | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- | | **Search** | Browse and search all threads (and files) in your team. | Always | | **Folders** | The folder hierarchy. See [Folders](/use/folders/). | Always | | **Assemblies** | Assembly explorer and package import. See [Assemblies](/use/assemblies/). | Only when the Assemblies module is enabled | | **Categories** | Cross-cutting thread groupings. See [Categories](/use/categories/). | Only when the Categories module is enabled | | **Import Threads** | Bulk-create threads from a CSV. See [Import](/use/import/). | Always | | **Thread Templates** | Manage reusable field templates. See [Threads](/use/threads/). | Always | | **Relationships** | Named links between threads. See [Relationships](/use/relationships/). | Always | | **Shipments** | Transfer threads to other organizations. See [Shipments](/use/shipments/). | Only when the Shipments module is enabled | | **Shared** | Items shared with your team, and items you shared. See [Sharing and Access](/use/sharing-and-access/). | Only when sharing is enabled | | **Updates** | Pending updates disclosed by sources of your threads. See [Disclosures](/use/disclosures/). | Only when provenance features (Shipments or Slicing) are enabled | | **Certificate Forms** | Design and generate certificates. See [Certificates](/use/certificates/). | Only when the Certificates module is enabled | | **Vlinks** | Reserve and manage permanent redirect links and their QR codes. See [Vlinks](/use/vlinks/). | Only for active-Team admins when the Vlinks module is enabled | | **Public Page Designs** | Build the reusable designs that public product pages are published through, and run Publish Waves. See [Public Pages](/use/public-pages/). | Only when the Public Pages module is enabled | ### Team Admin [Section titled “Team Admin”](#team-admin) | Entry | Where it goes | Availability | | --------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | **Connections** | Pair your team with teams in other organizations. See [Connections](/use/connections/). | Only when connections are enabled, and only shown to admins of the active team | Tip Some labels adapt to your organization’s terminology — for example, an organization may call threads something else, in which case entries like **New Thread** and **Thread Templates** use that name. ## Home page [Section titled “Home page”](#home-page) **Home** is the dashboard. Its header shows a live count of your team’s threads; clicking the count opens the thread list. * **Needs your action** — “Shipments, updates, and connections waiting on your team.” This queue collects inbound shipments to review, shipments where the recipient requested changes, failed deliveries (“retry or abandon”), your shipment drafts, pending updates from thread sources, and connections waiting on confirmation. Each item has a **Review**, **Open**, or **Continue** action. Shipment and connection items appear only for team admins, and each source appears only when its module is enabled; when everything is handled it says “You’re all caught up.” * **Pick up where you left off** — “Your working folder and recent threads.” Shows your working folder (with **Open folder**) and the threads you updated most recently (with **Open**). If you have nothing yet, it offers a **New Thread** shortcut. * **Recent Activity** — “Latest actions across your threads.”, with a **View all** button to the full [Activity](/use/activity/) page and **Newer** / **Older** paging. On mobile, Home splits into **Overview** and **Activity** tabs, and a quick-actions bar along the bottom of the app gives one-tap access to **New**, **Search**, **Scan**, **Folders**, and **Menu**. ## Context switcher [Section titled “Context switcher”](#context-switcher) The top bar always shows your active organization and team. Click it to see your **Current Context** and to use **Switch Organization** and **Switch Team**. A shield icon marks organizations and teams where you have the admin role. Switching is immediate and changes what every page shows. See [Getting started](/use/getting-started/) for details on first-time context selection. The top bar also has a **theme toggle** button that switches between light and dark mode, next to the user menu. ## User menu [Section titled “User menu”](#user-menu) Click your avatar in the top bar to open the user menu: * **Account Management** — opens your account profile in the identity service (new tab). * **Language** — switch the interface language. Currently **English** and **中文** (Simplified Chinese) are available; the choice applies immediately and is remembered. * **Sign out** — ends your session. ## Release Notes [Section titled “Release Notes”](#release-notes) DICE publishes customer-visible product updates on a public **Release Notes** page at `/release-notes` — no sign-in required. It is also linked from the footer of the landing page. Each release lists what changed, for example new modules, workflow improvements, and language support. Routine maintenance releases may use a shorter summary when changes are limited to bug fixes and improvements. ## Error pages and access [Section titled “Error pages and access”](#error-pages-and-access) If you open a link to something your current context can’t see, DICE explains why — for example “You don’t have access to this resource. This may happen if you recently switched contexts. The resource may belong to a different context.” Switching back to the right organization and team usually resolves it. # Public Pages > Design a reusable Public Page Design once, then publish verifiable public product pages for a single Thread or an entire product line. A **Public Page** is a public, unauthenticated web view of a Thread — the digital product passport a customer reaches by scanning an Identifier or following a printed link. What a page shows is decided by a reusable **Public Page Design** that you build once and apply to as many Threads as you like, so publishing a whole product line takes the same effort as publishing one item. Nothing is ever public by accident. A page’s address can be reserved — and its QR code printed — before there is anything behind it, and until you publish, a visitor reaches nothing but a notice that the page is registered. Once published, a page shows only the data its design explicitly pulls, and its content changes only when you publish again. Note Public Pages availability depends on your organization’s configuration. If you don’t see **Public Page Designs** in the sidebar, the module is not enabled for your organization (see the [FAQ](/use/faq/)). ## Who can do what [Section titled “Who can do what”](#who-can-do-what) Access follows your role in the active Team: * **Publishers** (and **Team admins**, who always include publisher access) build Page Designs, publish Design Versions and roll them out, publish and unpublish pages, and run Publish Waves. Making data public is the authority this role exists to control. * **All team members** can view designs, their version history, published pages, and page activity, but cannot change or publish anything. ## Page Designs [Section titled “Page Designs”](#page-designs) A Page Design is owned by a Team and determines both the content and the appearance of every page published with it. It is a stack of **blocks** in the order visitors read them: * **Hero** — the product media at the top of the page. Images, video, and audio all appear here; video and audio play inline and never autoplay, so a visitor on mobile data chooses when to load them. You can narrow it to images or videos alone (see [Choosing what the Hero shows](#choosing-what-the-hero-shows)). * **Identity** — the Thread’s name and description. * **Specifications** — the product attributes you want public, drawn from Thread fields. * **Documents** — public files such as spec sheets, warranties, and certificates. Images, video, and audio are left to the Hero, so nothing is listed twice. * **Text** — your own copy: provenance stories, care instructions, sustainability notes. * **Link list** — outbound links such as repair, recycling, or your brand site. * **Verification** — the DUST scan-verification panel. Verification starts from a button, so visitors are never asked for camera access just for opening the page. Inside the DUST Go app that button launches the scanner directly; in a phone browser it offers to open the page in DUST Go instead, with code and manual entry available as alternatives. * **Provenance** — the item’s story as a timeline. By default a page shows the history your team has recorded about the item’s life and the times the item itself was scanned and verified; everything else the record holds is opt-in, chosen per design (see [What the timeline shows](#what-the-timeline-shows)). Internal operations never appear on a public page: who a record was shared with, and which folders it has been filed in, are excluded whatever the design says. Blocks can be reordered, configured, and removed. Two things are always present and cannot be removed: the page chrome (the header carrying your logo alongside the platform’s “Powered by” mark, a sign-in link, and the footer) and the **record strip** showing the date the page’s data was last published — the marks that make a page recognizable as a genuine DUST record. The chrome carries no language or theme controls, and the record strip carries no code: a visitor holding the object is owed how current the record is, not a serial number to check. The Verification block is included in every new design and may be removed if a page is purely a brand story. ### What the timeline shows [Section titled “What the timeline shows”](#what-the-timeline-shows) The Provenance block is configured by choosing which parts of the item’s history the page publishes. Each choice is named by what it puts on the page: * **Recorded history** — entries your team recorded about the item’s life: sales, inspections, repairs (see [Recording past events](/use/recording-past-events/)). * **Verified scans** — times the item itself was physically scanned and checked. * **Ownership changes** — shipments and handovers between organizations. * **Parts and installation** — parts installed into or removed from a larger assembly. * **Certificates and validations** — certificates issued and documents validated for the item. * **Record activity** — edits to the record itself: details, documents, and identifiers changing. A new design starts with **Recorded history** and **Verified scans** — the story of the item leads, and the record’s own bookkeeping stays off the page unless you choose it. The designer’s live preview shows exactly what each choice publishes. Recorded entries appear as what they are: your organization’s attributed statement, marked **Declared**, never presented as something the platform verified. The date shows at the precision you actually claimed — “1968–1970”, “Circa 1835” — never a fabricated exact day, and the entry names the place you wrote, if you gave one. An entry you have retracted stays on the page struck through and marked **Withdrawn**: published provenance is never silently rewritten, so a visitor who saw a claim can also see that it was withdrawn. The only places a page ever shows are ones a publisher wrote into a recorded entry. Device positions — where a scan or an edit physically happened — are never published, at any precision. ### How designs find your data [Section titled “How designs find your data”](#how-designs-find-your-data) Specification, hero, and document blocks pull values by **field name**, not by a fixed template. A binding names the field it wants — for example `Metal` — with optional alternative names to try, and a type it must be. Applied across a product line, each Thread fills the same design with its own values. This is the same name-matching model Certificate Forms use, and it has the same practical consequence: the field names you use across a product line should be consistent, and renaming a field stops its binding from resolving on the next publish. Bindings are **optional by default** — if a Thread has no `Metal` field, that row is simply left off its page and everything else still publishes. Mark a binding **required** when its absence would break the page (a missing hero image, say); a Thread that cannot satisfy a required binding is reported rather than published with a hole in it. Because a design pulls only what it names, attaching a new file or field to a Thread never changes its published page on its own. A documents or hero block can instead be set to include all eligible public files, which is convenient for teams whose files are loose attachments — with the trade-off that new uploads then do appear the next time that page publishes. ### Choosing what the Hero shows [Section titled “Choosing what the Hero shows”](#choosing-what-the-hero-shows) The Hero block’s **media source** decides which of a Thread’s public media reaches the top of the page: * **Every public image, video, and audio file** — all public media on the Thread, images first. It names no fields, so it works across a product line whose files are simply attached rather than named consistently. Audio is included here because the Documents block leaves media to the Hero: if the Hero excluded it, a public audio file would appear nowhere on the page. * **Images only** — public images in Thread order, so a video attached to the Thread never leads the page. * **Videos only** — public videos in Thread order. * **One named media field** — exactly the image or video held by the field you name, for a design that must show a specific shot. The first option follows whatever is attached, which is what most teams want. The kind-narrowed options exist for when that is not a preference but a requirement: a product line where a walkthrough video is attached to some Threads and not others would otherwise lead with a video on exactly those items, and choosing **Images only** guarantees it never does. Whichever you pick, video and audio play inline and never autoplay. ### Appearance [Section titled “Appearance”](#appearance) Each design carries its own logo and accent colour, so a Team running two product lines simply keeps two designs. Upload a logo directly in the designer, or pick an image your Team already has — it is not attached to any Thread. The logo appears in the page header, where the page is co-branded: your mark leads and the platform credits itself alongside it. The header sizes the logo for you, so it behaves the same on a phone and a desktop. You can also set the page background, the header and footer bar colours, and pick a typeface for the page. A published page has one appearance — it does not follow the visitor’s device theme — so you lay it out once and every visitor sees that. Text is chosen for you so it stays readable on whatever colours you pick — a dark page gets light body text, labels and rules automatically — and accent colour is applied to page detailing only. Pages are shown in the visitor’s own language where the platform has one; your own content appears exactly as you wrote it. ### Previewing while you design [Section titled “Previewing while you design”](#previewing-while-you-design) The designer shows a live device-frame preview, and you can switch it between mobile and desktop to check both widths. Choose any Thread as a **design context** and the preview resolves that Thread’s real values through exactly the same machinery a real publish uses — what you see is what publishing produces. The preview also lists any bindings that do not resolve against the chosen Thread, so gaps are visible before you commit. ## Publishing a page for one Thread [Section titled “Publishing a page for one Thread”](#publishing-a-page-for-one-thread) ### Reserving the link before you publish [Section titled “Reserving the link before you publish”](#reserving-the-link-before-you-publish) A Public Page’s address is created before its content. Opening the Public Page action on a Thread that has none offers to **create a stable product link**: “Reserve a permanent URL and bind it to this thread. You can download or print its QR code before publishing any product data.” That order is deliberate — labels, engravings, and packaging are usually produced long before the product record is finished. A reserved link works from the moment it exists: a visitor who scans the printed code reaches a page saying the product page is registered but its information has not been published yet. A printed QR code is therefore never dead, and the address never changes. Publishing later fills in that same page. ### Publishing the data [Section titled “Publishing the data”](#publishing-the-data) 1. Open the Thread and choose the Public Page action. 2. Pick the Page Design to publish with. 3. Review the preflight summary. It lists every binding that resolved and every one that did not; required bindings that cannot resolve block publishing and say why. 4. Publish. The page becomes reachable at its permanent link, which you can copy, open, or print as a QR code. A Thread has at most one Public Page, and its link is permanent — republishing updates the content behind the same address, and the address survives unpublishing. The same action also states what is live right now as three separate facts: the design the page uses, the Design Version it is pinned to, and the date its data was last published. When the design has a newer version, the action says so and names the version it would move the page to — publishing one page always uses the design’s newest version, so correcting a single item also carries it forward. ## Publishing a product line [Section titled “Publishing a product line”](#publishing-a-product-line) To publish many Threads at once, start a **Publish Wave**: choose a design and a scope — a Folder, a Category, a Thread Template, or an explicit selection — and the wave creates and publishes a page for every Thread in it. Waves run in the background and are built for entire product lines, so you can start one and leave it. While a wave is running you can watch its progress and its published and failed counts. Publishing does not depend on you staying on the page: you can close it and come back later, and the wave screen tells you where it got to. A wave keeps expanding its scope as it works, so early on the queued total is still growing — the progress display says so rather than implying a total it does not yet know. When a wave finishes, it says so plainly and confirms how many pages are live. Threads that could not publish — usually a required binding that does not resolve — are listed individually with the reason, and you can retry just those once you have fixed the underlying data. That list appears only when there is something in it. A wave publishes through one version of the design, fixed when the wave starts, and every wave screen names that version. If someone publishes a newer version of the design while you are setting a wave up, DICE stops and asks you to review rather than quietly starting a wave on a version you never saw. ### Stopping a wave that is going wrong [Section titled “Stopping a wave that is going wrong”](#stopping-a-wave-that-is-going-wrong) A wave that has not finished can be **cancelled**, which is a publisher action. Cancelling stops every page the wave has not reached yet: the remaining work is retired and the run is recorded as cancelled. Cancelling is not an undo. Pages the wave already published keep the Design Version and the data it gave them, because rollout is forward-only — nothing is ever moved back. If those pages are wrong, correct the underlying design or data and publish forward again, either one page at a time or as a new wave. A wave that has already finished cannot be cancelled, for the same reason. ## Keeping pages current [Section titled “Keeping pages current”](#keeping-pages-current) A published page serves the data it was published with, so a page goes out of date as soon as its record moves on — a repair recorded, a field corrected, a document attached. This is separate from the design being out of date: a page can be sitting on the newest Design Version and still be showing older data than its record now holds. DICE tracks that for you. The designs list shows how many pages are **behind their records**, lists them, and offers **Republish all**, which is a publisher action. It covers every published page your Team owns, whatever design each one uses. Republishing refreshes the data without changing any page’s Design Version — it never rolls a design change out as a side effect, so clearing this list is always safe. A page is skipped rather than republished when its design no longer resolves against the record — a required binding whose field was renamed, say. A skipped page keeps serving its current publication and stays on the list, because a stale page is better than a broken one. Fix the underlying data and republish again. ### Living passports [Section titled “Living passports”](#living-passports) A design can be set to keep its pages current by itself. Turn on **Living passport** in the designs list and every page published with that design republishes automatically when new history is recorded on its Thread — a declared entry, or a retraction of one. It is the setting to use for a passport meant to read as the item’s live story rather than a snapshot of one moment. Automatic republishing is deliberately narrow: * It moves the data only. The page’s Design Version never changes, so a design still in progress cannot reach the public as a side effect of someone recording an event. * It is bounded per action. A large bulk recording republishes up to a few hundred pages inline; anything beyond that keeps serving its existing publication and appears in the behind-their-records list, where **Republish all** finishes the job. * It never breaks a live page. A page whose design no longer resolves is skipped, exactly as above. * It never publishes anything the design would not. The design’s timeline choices, private fields, and excluded documents all still apply — automatic republishing changes when a page updates, never what it may show. With Living passport off, pages stay frozen until someone republishes them, which is the right default for a certificate-like page that should change only when a person decides it should. ## Editing a design after pages are live [Section titled “Editing a design after pages are live”](#editing-a-design-after-pages-are-live) Published pages never change underneath you. A design’s edits are saved as a draft that affects nothing public until you **publish the design**. ### Publishing a design freezes a version [Section titled “Publishing a design freezes a version”](#publishing-a-design-freezes-a-version) Publishing a design freezes its saved draft as a numbered **Design Version** — v1, v2, v3 — and that version never changes again. Publishing on its own changes nothing the public sees: every live page stays on the version it is already using until you roll the new one out. So you can publish a version as soon as it is ready and decide separately when your pages should move to it. Within Public Pages, “version” names exactly this and nothing else. A page’s own publishing history is dated rather than numbered. ### Rolling out a version [Section titled “Rolling out a version”](#rolling-out-a-version) **Roll out** is the single action that moves a design’s pages onto its newest version. You choose the design and, if you want, narrow it to a scope; DICE does whichever of these the change requires: * **Appearance and wording only** — reordering blocks, changing text, labels, colour, or logo. Rolling out is instant: pages move to the new version without republishing any page data. * **What the page pulls** — adding or changing bindings, changing a block’s file source, or changing which history a page shows. Rolling out re-publishes each page from its Thread’s current data, as a Publish Wave you can watch. DICE tells you which of the two you are looking at when you publish the design, so you know what rolling out will involve before you start it. You never pick the mechanism — you pick the version and the scope. Rolling out is a publisher action, and it is a fleet-scale one: it belongs to the design, not to an individual page. ## Versions and history [Section titled “Versions and history”](#versions-and-history) Each page keeps its five most recent publications — plus whichever one is currently live, however old it is — so you can see what it showed previously; the most recent publication is what visitors see. The trimming happens as part of publishing: each time a page publishes, anything older than that is discarded there and then, so a page republished by every wave never grows without bound. A publication is identified by the date and time it was published, never by a number — numbers belong to Design Versions alone. ### Latest and in use [Section titled “Latest and in use”](#latest-and-in-use) A design’s **latest** version is simply the newest one published. The versions **in use** are the ones live pages are actually on. These are different facts, and DICE labels them separately wherever a version appears, so a version number is never mistaken for “what the public sees”. Because rolling out is a deliberate act and waves can be scoped, one design is normally in use on several versions at once — one product line still on v2 while a newer line sits on v3. That is the expected steady state rather than a problem to fix, and the designs list flags a design whose pages have not all moved to the latest version, so you can finish the rollout when you choose to. A design’s **version history** lists every version it has published, who published it, when, and how many pages are on each, and lets you read any version’s frozen contents. It exists for audit and diagnosis, so every team member can open it; publishing and rolling out stay with publishers. ### Version history is a record [Section titled “Version history is a record”](#version-history-is-a-record) Version history is a record of what happened, not a set of save points. A published version is never brought back, and a page is never returned to older data. If a version is wrong, correct the design, publish a newer version, and roll that one out — which is always available and takes the same two steps as any other change. This is deliberate. Pages are published from Thread data as it stands at that moment, so returning to an older Design Version would reproduce that version’s layout filled with today’s data — never the page that was actually live. Moving forward is the only action that means what it says, so it is the only one offered. ## Unpublishing [Section titled “Unpublishing”](#unpublishing) Archiving a Public Page stops serving it immediately. The page’s link keeps existing but returns a generic not-found response — the same response an unknown link gets, so nothing is revealed about the item. Its configuration and design assignment are kept, so republishing later is a single action. Unpublishing is also the way to take one item’s page down when its history or data should not be public: pages are produced by a shared design, so the control for one page is whether it is published at all. ## What visitors can and cannot see [Section titled “What visitors can and cannot see”](#what-visitors-can-and-cannot-see) * Visitors see only what the design pulls, and only for a published page. * Files are delivered so that they can never run as code in a visitor’s browser, and a file the page does not display cannot be fetched through it. * Making a **file** private or archiving it stops it being served straight away — without waiting for a republish. Its entry can still be listed on the page until the next publish, where it will no longer resolve. * Making a **field** private, or hiding an event, takes effect on the next publish. The same is true of retracting a recorded entry: the published page keeps showing the claim as it stood until you publish again, after which it appears struck through and marked Withdrawn. A published page serves the snapshot it was published with, so use Unpublish if something needs to come down immediately. * Public pages are not indexed by search engines. * Page views and verification scans are recorded for you as page activity. The public page never shows visitor information to anyone, and no visitor identity is collected. # Recording past events > Add things that happened outside DICE — from a moment ago to centuries past — to a thread's Transaction History as declared, attributed entries. A thread’s Transaction History normally records what DICE itself observed. **Recording** lets you add what happened *outside* DICE: a repair at a vendor last March, a sale before the item was ever tagged, or a painting created in 1835. Each recorded entry becomes a permanent part of the Transaction History, interleaved at the time you claim it happened. Recorded entries are always marked **Declared** in the feed. They are your attributed statement — DICE never verifies them, and never presents them as facts it observed. ## Recording something that happened [Section titled “Recording something that happened”](#recording-something-that-happened) 1. Open the thread and select **Record** at the top of the Transaction History. 2. Give the entry a short title — for example, *Repackaged for shipment*. That’s enough: press **Add to record**, and the entry lands in the history as of now. 3. Optionally add details, the kind of event (inspection, sale, repair, …), and where it happened, under **More options**. Caution A recorded entry is part of the permanent record: it is shared with future owners along with the rest of the thread’s history, and it cannot be deleted — only retracted. ## Recording the past [Section titled “Recording the past”](#recording-the-past) The **When** control accepts more than “now”: * **A date** — as precise as you actually know: a year (`1835`), a month (`1835-06`), an exact day (`1835-06-15`), a range (`1968` to `1970`), or an open bound (before/after a date). Check **Approximate** when the date is a “circa”. * **Relative to an entry** — place the record just before or just after an existing entry in the history, when you know the order of events better than their dates. The entry sorts into the timeline at the claimed time — a creation date of 1835 appears before the thread’s own creation — and the feed always displays the claim at the precision you stated (“Circa 1835”, “Before March 1970”), never a fabricated exact timestamp. ## Recording many entries at once [Section titled “Recording many entries at once”](#recording-many-entries-at-once) When you have a whole history to add rather than one entry, upload it as a CSV. The file describes **events only** — it never names which records receive them. That comes from where you start the import: * **One record, many entries** — open **Record** on the thread and switch to **Import CSV**. Every row lands on that thread. * **Many records, the same history** — select the records, then choose **Import past events (CSV)** from the batch actions. Every row lands on every selected record, so 3 rows across 40 records is 120 entries. The review step states that number before you commit. The columns match the single-entry form: *Title, Details, Kind, When, Location, Latitude, Longitude*. Nothing is required as long as each row carries a title, details, or a kind. **When** takes the same date claims as the form, written as EDTF — `1968`, `1968-03`, `1968-03-21`, `1968/1970`, `1835~`, `../1970`, `2019/..` — and a row with no date is recorded as of now. **Location** publishes exactly as written and is never looked up on a map. Download the example CSV from the upload step to start from a working file. Caution An import is all-or-nothing: if any row has a problem, the review step names it and nothing is imported. Because entries are permanent, this is deliberate — fix the file and upload it again rather than cleaning up a half-applied import. Once an import succeeds, **Undo** on the confirmation retracts the whole thing in one step. A CSV can carry up to 100 rows, and one import can write up to 2,000 entries. For a continuous feed from an ERP, WMS, or MES — or for many records each with a *different* history — post entries through the API instead; see [Provenance from an ERP](/integrate/erp-provenance/). ## Correcting a mistake [Section titled “Correcting a mistake”](#correcting-a-mistake) Recorded entries can’t be edited or deleted — like everything in the Transaction History, they are immutable. Instead, **retract** the entry from its expanded view, and record a corrected entry in its place. A retraction is visible from both sides of the history: * The **retracted entry** stays where it was, struck through, greyed, and marked **Retracted**. Expanding it shows who retracted it, when, and the reason given. * The **retraction itself** appears as its own entry at the moment you retracted, titled with the entry it retracts (*Retracted: Repackaged for shipment*) and linking back to it. Recording and retracting require edit access to the thread. Viewers see recorded entries like any other history. # Relationships and linked threads > Define relationship types with forward and inverse labels, link Threads together, and review or remove links. Relationships describe how [Threads](/use/threads/) relate to each other — “blocks / is blocked by”, “parent of / child of”, “supersedes / is superseded by”. A relationship is defined once for your Team, then used any number of times to link pairs of Threads. Each link is directional: one Thread carries the forward label, the other the inverse. ## Relationship definitions [Section titled “Relationship definitions”](#relationship-definitions) A relationship type has four parts, set on the **Relationships** page: * **Name** — the type’s name (e.g. “Blocking”, “Dependency”, “Hierarchy”). * **Description** — optional context for your Team. * **Forward Label** — how the link reads from the source Thread (e.g. “blocks”, “depends on”, “parent of”). * **Inverse Label** — how it reads from the other side (e.g. “is blocked by”, “is depended on by”, “child of”). As the form notes: “Relationship labels appear exactly as written in thread history and relationship pickers”, so word them the way you want them read. ## Create a relationship type [Section titled “Create a relationship type”](#create-a-relationship-type) 1. Open **Relationships** in the navigation. 2. Click **New Relationship**. 3. Fill in **Name**, optional **Description**, **Forward Label**, and **Inverse Label** — “Both labels will appear in the dropdown when linking items.” 4. Click **Create**. Each relationship type appears as a card showing how many links use it (“N links using this relationship”) with actions to **Edit** the labels and description, **View Links**, or **Archive** it. Archived types keep their existing links but disappear from pickers; toggle **Show Archived** to see them and **Unarchive** to bring one back. ## Link Threads from a Thread’s page [Section titled “Link Threads from a Thread’s page”](#link-threads-from-a-threads-page) Linking happens on a Thread’s detail page, in its **Relationships** section (“Related threads and dependencies”). 1. In the **Relationships** section, open the **Add Relationship** dialog — “Choose a relationship type and Thread to link.” 2. Pick the relationship type. You can search the list, or click **Create A New Relationship** to define one without leaving the dialog. 3. Pick a direction — forward or inverse label — for how the new links should read from this Thread. 4. Select the target Threads (see below). Selected Threads collect in a **Pending selection** panel; remove any you didn’t mean to add. 5. Click **Add Relationships** (the button counts the pending links, e.g. **Add 3 Relationships**). Changing the relationship type or direction mid-way clears the pending selection (after a “Clear pending selection?” confirmation), since the checks for what’s already linked depend on both. ### Selecting targets: Browse or Identify [Section titled “Selecting targets: Browse or Identify”](#selecting-targets-browse-or-identify) The target picker has two tabs: * **Browse** — a searchable Thread table (with a **Show archived** toggle). Multi-select the Threads to link. * **Identify** — pick the target by scanning its identifier, using the same **DUST**, **Camera**, or **Manual** scanner modes as the main scanner (see [Identifiers and scanning](/use/identifiers-and-scanning/)). A successful scan adds the matched Thread to the pending selection (“added to pending selection — will be linked when you confirm”). Scanned Threads are validated before they’re added. The scanner will refuse a Thread that: * is the Thread you’re linking from (“Cannot link to itself”), * is not owned by the same Team as the anchor Thread (“Not available for linking”), * is archived while **Show archived** is off, * is already in the pending selection, or already has this relationship and direction (“Already linked”). ### Linking on shared Threads [Section titled “Linking on shared Threads”](#linking-on-shared-threads) When you’re adding links on a Thread another Team shared with you, the relationship types come from the owning Team (“Choose a relationship type from the owner and link it to another Thread you can edit”) — you can’t create new types for someone else’s Thread. See [Sharing and access](/use/sharing-and-access/). ## View all links for a relationship [Section titled “View all links for a relationship”](#view-all-links-for-a-relationship) From a relationship card, **View Links** opens a table of every link using that type, with columns **Relationship**, **Related Thread**, and **Created**. You can search by Thread name and filter by type (**All relationships** by default). Note the direction caveat shown on the page: “This table shows links grouped by the direction in which they were created.” ## Remove a link [Section titled “Remove a link”](#remove-a-link) In a Thread’s **Relationships** section, each linked Thread has an **Unlink** action. Unlinking asks for confirmation — “This action cannot be undone and will be tracked in the Thread history.” Both the creation and removal of links appear in [transaction history](/use/activity/) as “Linked Thread” and “Unlinked Thread” events. ## Relationships vs. assembly positions [Section titled “Relationships vs. assembly positions”](#relationships-vs-assembly-positions) Relationships and [assemblies](/use/assemblies/) both connect Threads, but they do different jobs: | | Relationship link | Assembly part | | --------- | ------------------------------------------------------------- | ------------------------------------------------------- | | Meaning | Descriptive: any labeled association between two peer Threads | Structural: a part is installed into a parent assembly | | Direction | Forward/inverse labels you define | Always part-of-assembly, optionally at a named position | | Access | No effect — each Thread keeps its own access | Parts inherit the assembly’s access | | Ownership | Both Threads must be owned by the same Team | Parts must share the assembly’s owner | Use a relationship for “these two things are related in this way”; use an assembly when one Thread is physically or logically **built into** another. ## Related pages [Section titled “Related pages”](#related-pages) [Assemblies](/use/assemblies/)Structural part-of composition between Threads. [Identifiers and scanning](/use/identifiers-and-scanning/)The scanner used by the Identify picker. [Activity](/use/activity/)Where link and unlink events are recorded. # Sharing and access > Give other teams access to your threads, folders, and categories, review who can see what, and manage inherited or suspended shares. Sharing gives another team access to a thread, folder, or category **without moving ownership**. Your team stays the owner; the other team sees the item in their **Shared** view and can open it with the role you granted. That makes sharing the collaboration tool — to hand items over for good, use a [shipment](/use/shipments/) instead. Sharing and shipping both move data over a Connection, but they are different acts: a share is a window into your live Thread; a shipment hands the receiving team their own copy. Note Sharing availability depends on your organization’s configuration — if you don’t see a **Share** button or a **Shared** section in navigation, it isn’t enabled for your team. See the [FAQ](/use/faq/). ## Who you can share with [Section titled “Who you can share with”](#who-you-can-share-with) * **Teams in your own organization** — always available, no setup required. * **Teams in other organizations** — only after your organizations are linked by a [connection](/use/connections/), and only if the connection’s data-flow direction lets your team send. When it doesn’t, the team appears in the share dialog but is disabled with the reason **“Connection does not allow sharing data with this team.”** ## Roles [Section titled “Roles”](#roles) Each share grants one role, chosen from the **Relation** picker: | Role | What it allows | | ---------- | -------------------------------- | | **Viewer** | Open the item and read its data. | | **Editor** | Open the item and make changes. | ## Share a thread, folder, or category [Section titled “Share a thread, folder, or category”](#share-a-thread-folder-or-category) 1. Open the item and click **Share** in its header (on items you can’t manage, the button reads **Access** instead — see below). 2. In the dialog, find **“Share with a connected team”** and select one or more teams. Teams that already have a direct share are managed in the access list instead; a team that only inherits access shows **“Already inherits access — sharing adds a direct grant.”** 3. Pick a role in the **Relation** picker for each selected team. 4. Click **Share**. A confirmation such as **“Thread shared”** appears, and the item shows up in the other team’s **Shared** view. Folders and categories share everything inside The dialog reminds you of the blast radius: *“Anyone you share this folder with can open everything inside it — all threads and subfolders.”* and *“Anyone you share this category with can open every thread in it, including threads in its subcategories.”* ## The Access view [Section titled “The Access view”](#the-access-view) The same dialog is dual-mode: * **If you can manage sharing** on the item, it opens as **“Share …”** with the description **“Manage access to this thread.”** (or folder/category) and shows the full share controls. * **If you can’t** (for example, you’re a viewer or editor on a thread another team shared with you), it opens as a read-only access summary — **“Thread access”** — with the note **“Read-only — only teams who can share this thread can change who has access.”** Both modes show the **“Who can access”** list: *“Everyone who can open this thread, and how.”* Each team entry is labeled with how it got access: * **Owner** — the owning team (“Owns this thread”). * **Shared directly** — a direct grant, shown with its role, e.g. **“Shared directly · Viewer”**. * **Inherited from …** — access that flows from a container, e.g. **“Inherited from folder “Receiving” · Editor“**. ## Inherited access [Section titled “Inherited access”](#inherited-access) Sharing a folder, category, or assembly automatically extends access to what’s inside it. On an individual thread, the Access view shows these grants as **“Inherited from”** the folder, category, or assembly they come from. Inherited access can’t be removed on the thread itself — remove or change the share on the container instead. If a team has both a direct share and inherited access, removing the direct share keeps the inherited access; the dialog warns: **“Removing the direct share won’t revoke access — still inherited from …”**, and the confirmation reads **“Direct share removed — inherited access remains”**. ## The Shared page [Section titled “The Shared page”](#the-shared-page) The **Shared** entry in navigation opens two tables: * **“Shared with team”** — *“Items that others have shared with you.”* Columns include **Item**, **Permission**, **Shared By**, and **Owner**. This is where anything a partner team or a sibling team granted you appears. * **“Shared”** — *“Items you have shared with others.”* Columns include **Item**, **Permission**, and **Shared With**, so you can audit everything your team has granted out. Both tables are searchable by item name. ## Suspended shares [Section titled “Suspended shares”](#suspended-shares) Cross-organization shares depend on the [connection](/use/connections/) between the two teams. A share can become suspended in two ways: * **Connection paused** — the share is labeled **“Suspended — connection paused”**: *“The connection is paused, so this share is suspended and can’t be edited. It resumes automatically when the connection is resumed.”* * **Direction no longer allows it** — the share is labeled **“Suspended — direction not permitted”**: *“The connection no longer allows sending data to this team, so this share is paused and can’t be edited. Update the connection direction to restore it.”* While suspended, the other team loses access, but nothing is deleted — restoring the connection (resuming it, or amending its direction) restores every suspended share automatically. ## Unshare [Section titled “Unshare”](#unshare) In the manage view of the dialog, use **Manage** next to a team and choose **Remove direct share**. The confirmation reads **“Thread unshared”** (or folder/category). Remember that inherited access survives — revoke it at its source container. ## Related [Section titled “Related”](#related) [Connections](/use/connections/)Link your team with a partner organization before sharing across org boundaries. [Shipments](/use/shipments/)Transfer ownership of threads to another organization instead of sharing access. [Threads](/use/threads/)The items you share: threads, their fields, files, and identifiers. # Shipments > Transfer threads and assemblies — with their fields, files, and identifiers — to a team in another organization, and receive what other teams ship to you. A shipment transfers threads (including whole assemblies) to a team in another organization, together with the data you choose to include — fields, files, and identifiers. As the Shipments page puts it: *“Ship threads to teams in other organizations and review what other teams send to you. You keep your originals — a shipment creates copies owned by the receiving team.”* Shipments require an active [connection](/use/connections/) whose direction allows your team to send to the recipient. Note Shipments availability depends on your organization’s configuration — if you don’t see **Shipments** in navigation, the module isn’t enabled (see the [FAQ](/use/faq/)). Even when enabled, *“viewing and managing them requires a team admin role.”* ## The Shipments page [Section titled “The Shipments page”](#the-shipments-page) The page has two tabs: * **Outbound** — shipments your team is sending, filterable by **All**, **Drafts**, **In transit**, and **Completed**. * **Inbound** — shipments other organizations send your team, filterable by **All**, **Needs review**, **Processing**, and **Received**. ![The Shipments page on the Outbound tab with a draft shipment listing its recipient, status, and item count](/_astro/shipments.DlKIsxrn_Z1BPFt2.webp) The Shipments page: outbound and inbound tabs, with each shipment's recipient, status, and manifest size. Every shipment shows a status: **Draft**, **Awaiting response**, **Changes requested**, **Processing**, **Completed**, **Rejected**, **Canceled**, **Processing failed (retrying)**, or **Abandoned**. A stepper on the detail page tracks progress through **Draft → Sent → Processing → Complete**. The life of a shipment. The receiving team can accept, reject, or request changes; a failed transfer can be retried or abandoned, and an unaccepted shipment can be canceled. ## Compose a shipment [Section titled “Compose a shipment”](#compose-a-shipment) 1. Start a draft: click **New shipment** on the Shipments page, or select threads anywhere in DICE and use the batch bar’s **Add to shipment** action (which can add to an existing unsent shipment or start a new one). Give it a **Name** and optional **Description** — *“Context the receiving team should see.”* 2. Choose the recipient. The picker lists connected teams; a team the connection doesn’t permit you to send to is disabled with **“Connection does not allow sending data to this team”**. If you have no connections yet, set one up first. 3. Build the **Manifest** — *“What the receiving team gets.”* Use **Add threads** to search your team’s threads or scan an identifier. A thread can only be shipped once: already-shipped threads are hidden, and *“threads already in another active shipment are shown but can’t be selected”* (they link to the shipment holding them). 4. Optionally **Customize** each item — *“Choose what travels with this shipment.”* Pick exactly which **Fields**, **Files**, and **Identifiers** to include; for assemblies, include or exclude individual parts and choose assets per part. *“Excluding a part leaves out everything installed in it. Excluded parts stay with your team and can be transferred separately.”* Changes save automatically. 5. Optionally mark one item as the **Primary thread** — the headline item of the shipment, shown with its own summary card. It stays in the manifest if you later **Remove as primary**. 6. Click **Send shipment**. The status becomes **Awaiting response**: *“Waiting for the receiving team to review and respond.”* Private data never ships *“Private fields, files, and identifiers are excluded automatically and can never be included.”* Your threads stay in your workspace and remain editable until the shipment is sent. Once sent, the manifest is locked and your source threads are locked too: a thread in a sent shipment *“cannot be edited until the shipment is sent back for changes, canceled, or rejected.”* ### Messages [Section titled “Messages”](#messages) Each shipment has a **Messages** panel both parties can post to — note the visibility warning: *“Anyone at the other team can read what you post here.”* Responses like rejections, change requests, and abandonments also appear here with their notes. ## Receive a shipment [Section titled “Receive a shipment”](#receive-a-shipment) Inbound shipments needing review show three actions: **Accept**, **Request changes**, and **Reject**. Before deciding, review the preview — **“What you will receive”** — a per-thread breakdown of the copies the shipment would create (fields, files, and identifiers), plus any provenance disclosure, including certificates. Items marked **Thread copy** *“become your team’s own data when you accept”*; items marked **Provenance disclosure** are *“shown through provenance after you accept; not copied as your data.”* * **Accept** — the point of no return. The dialog **“Accept this shipment?”** warns: *“Accepting starts processing immediately: copies of the included threads, fields, files, and identifiers are created in your team, and DUST identifiers are registered to your organization. Once processing starts, the shipment can no longer be rejected or canceled.”* * **Request changes** — returns the shipment to the sender for editing; a reason is required *“so the sender knows what to fix.”* The sender edits the manifest and uses **Resend shipment**. * **Reject** — *“Nothing is copied to your team. The sender keeps their threads and can start a new shipment later.”* An optional note can be included. ## After acceptance [Section titled “After acceptance”](#after-acceptance) Accepting starts **Processing**: *“Copies are being created for the receiving team. All items ship together.”* The page updates automatically and it’s safe to leave — processing finishes on its own. When it completes: * **Receiving team** — the copied threads are yours. Select received threads on the shipment page to organize them with **Move to folder** and **Add to category**. * **Sending team** — the shipment becomes your receipt — *“This is your shipment receipt. It records what you shipped … and when.”* Your source threads are marked shipped — *“The receiving organization now works from its own copy. You keep this original, but it can never be shipped again.”* Your originals stay editable, but changes reach the recipient’s copies only through an explicit disclosure push — see [Disclosures](/use/disclosures/). The transfer itself is recorded permanently in the [Fabric](/use/fabric/) lineage of both sides’ threads. ## If processing fails [Section titled “If processing fails”](#if-processing-fails) Processing is all-or-nothing. If it fails, the shipment shows **“Processing failed”**: *“This accepted shipment didn’t finish processing. Nothing was created on the receiving team. Retry to run it again, or abandon it to release its threads.”* The last error is shown on the page. The sender has three options: * **Retry** — runs processing again on the same accepted shipment. * **Abandon** — gives up on this attempt: *“This gives up on the accepted attempt and releases its threads so they can be shipped again.”* A reason is required and *“recorded in the shipment’s history.”* * **Start new shipment** — available on a failed shipment; creates a fresh draft from this shipment’s manifest (*“New shipment draft started from this manifest.”*), so you can adjust and send again without rebuilding the item list. ## Cancel a draft [Section titled “Cancel a draft”](#cancel-a-draft) An unsent shipment can be canceled at any time. The dialog **“Cancel this shipment?”** confirms the stakes: *“The draft and its manifest are discarded. Your threads are not affected.”* ## Related [Section titled “Related”](#related) [Connections](/use/connections/)The team-to-team link a shipment travels over — direction controls who can send. [Fabric](/use/fabric/)Where the transfer is recorded as permanent lineage on both sides. [Disclosures](/use/disclosures/)Push later updates from your source threads to the receiving team. [Assemblies](/use/assemblies/)Ship an assembly and its parts together, or detach parts first. # Slicing > Derive new threads from an existing thread — copying selected fields, files, and identifiers — while Fabric records the lineage automatically. Slicing derives one or more new threads from a source thread: *“Derive a new thread from this one. The source stays unchanged and can be sliced again.”* It’s the tool for physical subdivision and spin-offs — cutting rods from a bar, splitting a lot into serialized units — where each new item needs its own thread but should stay traceably linked to where it came from. Unlike a [shipment](/use/shipments/), a slice is a **same-team derivation**: the new threads are owned by the source’s team, and *“ownership of the source is unchanged.”* The derivation is recorded in [Fabric](/use/fabric/) as lineage. Note Slicing availability depends on your organization’s configuration — if you don’t see slice actions on your threads, the module isn’t enabled (see the [FAQ](/use/faq/)). ## Where to slice from [Section titled “Where to slice from”](#where-to-slice-from) * The **Lineage** card on a thread’s detail page has a **New slice** action. * The Fabric provenance explorer offers **Slice this thread**. You can only slice a thread you can **edit** — this includes threads another team shared with you as an editor (see [Destination folder](#destination-folder) for where those slices land). Two states make a thread temporarily or permanently unavailable as a slice source: * **Already shipped** — *“This thread has already been shipped, so your team no longer has the active inventory copy to slice from. Create new slices from the current holder’s thread instead.”* * **Locked in a shipment** — *“This thread is locked in a shipment that has already been sent or is processing. Slicing becomes available again if the shipment returns for changes or closes without completing.”* ## The slice composer [Section titled “The slice composer”](#the-slice-composer) Opening a slice action lands you in the composer (**“Slice "…"”**). Under **“What to create”** there are three modes: ### One slice [Section titled “One slice”](#one-slice) Give the new thread a **Name** (a default like **“Slice 1”** is suggested) and an optional **Description**, then click **Create slice**. ### Many slices [Section titled “Many slices”](#many-slices) Create a numbered batch in one pass: 1. Set a **Quantity** and a **Name pattern** (for example `Titanium Rod {n}` — `{n}` is *“the slice number”*). 2. Click **Generate rows**, then edit any individual name or description in the table. Up to **100** slices at once, *“created one after another. They share the data and folder below.”* 3. Click **Create slices**. ### From CSV [Section titled “From CSV”](#from-csv) Drive a batch from a spreadsheet: *“One row per slice.”* Upload a **CSV file**, then map columns: * **Name column** (required) — *“Choose which column names each slice.”* * **Description column** (optional). * **Existing fields** — *“Override a copied field’s value from a column. Empty cells keep the source’s value.”* * **New fields** — *“Add a field the source doesn’t have and fill it from a CSV column — it’s created on every slice.”* Each new field gets a name, a type, and a source column. Up to **100** rows, *“created in one batch.”* The composer validates every row and lists issues to fix before **Create slices** is enabled. ## Thumbnail [Section titled “Thumbnail”](#thumbnail) Choose how the new threads should appear in lists and Fabric: * **Use source thumbnail** (the default) carries the source thread’s current thumbnail forward. * **No thumbnail** creates the slices without one. * **Upload image** attaches the chosen image to every slice and uses that attached file as its thumbnail. ## Choose what to copy [Section titled “Choose what to copy”](#choose-what-to-copy) The **“Copy which data?”** card lists the source’s **Fields**, **Files**, and **Identifiers**, each individually selectable. Everything eligible is pre-selected; two kinds of assets can never be copied: * **DUST identifiers** — *“DUST identifiers represent a physical identity and are never copied.”* A slice is a new physical item; bind it to its own identifier. * **Private assets** — marked *“Private — can’t be copied.”* As the card summarizes: *“The source thread stays unchanged. DUST identifiers and private data are never copied.”* ## Update the source thread [Section titled “Update the source thread”](#update-the-source-thread) Optionally, the composer lets you adjust the source in the same operation: *“Optionally adjust the source as you slice from it — e.g. a bar’s remaining length once rods are cut off. Blank fields are left unchanged.”* Enter new values only for the fields that changed; use *“Leave blank to keep current”* for the rest. ## Destination folder [Section titled “Destination folder”](#destination-folder) A **Folder** is required — *“The new thread will be filed here.”* (or, for a batch, *“All new threads will be filed here.”*). If you’re slicing a thread that another team shared with you, the result belongs to them, not you: *“The slice is created in the owning team’s workspace. Pick a folder they shared with you”* to file it into. ## Where the lineage shows up [Section titled “Where the lineage shows up”](#where-the-lineage-shows-up) After creating, each source–slice relationship is permanent Fabric lineage: * The source thread’s **Lineage** card lists the new threads under **“Derived from this thread”**, each row tagged **Slice** with its creation time. Each slice’s own card shows the source under **“Created from”**. * In the Fabric provenance explorer, the slice appears as a **Slice** link in the graph — *“Same-team derivation; ownership of the source is unchanged.”* — and the full chain (slices of slices, later transfers) stays navigable from any thread in it. See [Fabric](/use/fabric/) for how to read and explore provenance. ## Related [Section titled “Related”](#related) [Fabric](/use/fabric/)Explore the provenance graph that records every slice and transfer. [Shipments](/use/shipments/)Transfer threads to another organization — the cross-org counterpart to slicing. [Threads](/use/threads/)Fields, files, and identifiers — the assets a slice copies. # Tamper Analysis > Compare a DUST identifier's current scan against the reference captured when it was bound, review the marker measurements and evidence layers, and record your own Tamper Observation. A **Tamper Analysis** compares a fresh scan of a DUST identifier against the reference captured when that identifier was **bound**, and reports what it measured: how much of the reference marker pattern the new scan accounts for, plus the visual evidence layers behind those numbers. It stops there. DICE never states whether the identifier was tampered with. The only conclusion in the system is a **Tamper Observation** — a record *you* write, in your name, saying what you concluded from the evidence in front of you. Note Tamper Analysis availability depends on your organization’s configuration. If you don’t see a tamper analysis action on your DUST identifiers, the module is not enabled for your organization (see the [FAQ](/use/faq/)). ## Why DICE reports measurements and leaves the conclusion to you [Section titled “Why DICE reports measurements and leaves the conclusion to you”](#why-dice-reports-measurements-and-leaves-the-conclusion-to-you) Whether an identifier has been tampered with is a judgment about a physical object — it depends on how the item was handled, how much wear is expected of it, how it was packaged, who held it and for how long, and what it is worth — and DICE can see none of that, so it reports only what it measured and leaves the conclusion to the person holding the item. That single decision shapes the whole module: * There is **no summary number** for an Analysis, no rating, and no percentage that means “good” or “bad”. * There is **no threshold**. DICE never says a measurement is high, low, normal, or unusual, because any such line would be DICE deciding on your behalf where acceptable ends. * **Color distinguishes the marker classes and nothing else.** The legend in the viewer says so explicitly: no color in the module encodes severity, and none of them means “worse”. * **The measurement labels describe what was counted**, never what happened to the item. A marker missing from the new scan is reported as missing from the new scan — not as removed, altered, or damaged. * An Analysis is never described as passing or failing, and it is never reduced to a single current state. Every Analysis of an identifier is kept, and so is every Observation written from it. Because of this, the module rewards a reader who knows the item and its handling history. Someone who cannot interpret marker coverage will not get an answer from DICE — by design. ## Running a Tamper Analysis [Section titled “Running a Tamper Analysis”](#running-a-tamper-analysis) A Tamper Analysis needs a DUST identifier that is already bound to a Thread; the other identifier types (QR, barcode, data matrix, NFC) have no marker pattern to compare, so the module does not apply to them. 1. Open the Thread and select the DUST identifier you want to analyze. 2. Start a tamper analysis and scan the physical identifier, exactly as you would to [verify](/use/identifiers-and-scanning/) it. An ordinary DUST scan is valid input — there is no special capture mode to learn. 3. DICE compares that scan against the reference captured when the identifier was bound, and records the measurements as a permanent Analysis. Each Analysis is immutable and attributed to whoever ran it. Running another one never replaces an earlier one: re-scanning an identifier a week later gives you two Analyses to compare, not an updated result. An Analysis belongs to one identifier, so a Thread carrying more than one DUST identifier asks you to choose before the scanner opens. The identifier’s name, description, and DUST value are shown when you pick it, again in the scanner, and on the Analysis afterwards — identifiers are often unnamed, and the value is what tells two of them apart. ## What the measurements tell you [Section titled “What the measurements tell you”](#what-the-measurements-tell-you) DUST identifiers carry a pattern of microscopic **markers**. The Analysis reports how the markers in your new scan line up with the markers in the reference captured at Bind, as three coverage measurements: | Measurement | What it counts | | ----------------------------------------- | ---------------------------------------------------------------------------- | | Reference markers also found in this scan | Markers present in the Bind reference that the new scan accounts for | | Reference markers not found in this scan | Markers present in the Bind reference that the new scan does not account for | | Markers in this scan not in the reference | Markers the new scan shows that the Bind reference does not contain | Alongside them, DICE reports how many markers were considered on each side, so you can see how much evidence the measurements rest on. These are coverage counts, not causes. Markers can go unaccounted for because a surface was dirty, worn, wet, partly obscured, or scanned at an awkward angle or in poor light, as well as because the surface itself changed. Nothing in the numbers distinguishes those situations, which is precisely why DICE does not draw the conclusion for you — and why re-scanning under better conditions is often the most useful next step. ## Reading the evidence layers [Section titled “Reading the evidence layers”](#reading-the-evidence-layers) The evidence viewer stacks the scan imagery and the marker classes so you can look at the measurements rather than take them on trust: * **It opens on the measured markers.** All three marker classes are drawn over the scan, with **Surface difference** behind them — where the two scans differ across the whole scanned area, not only at the markers — so an Analysis shows its evidence without you configuring anything first. * **Markers and density toggle independently.** Each marker class has its own marker overlay and its own **Density** wash, showing where that class’ markers concentrate. Show one class on its own, or compare two, without the others in the way. Alt-click a class in the legend to isolate it. * **The base image switches between the two scans.** The reference capture (taken at Bind) and the scan you just took are both shown in the same frame, so you can read the same overlays against either surface — or against none — and nothing moves when you switch. Older Analyses may have only the new scan recorded. * **Marker positions are told apart by shape**, not color: circles for the reference capture, diamonds for this scan. Each carries the number of coordinates it draws, so you can always tell which set you are looking at. * **Zoom, pan, and rotation work across the whole composite**, with every enabled layer staying in register — that is how you examine one area of the surface rather than a whole-surface average. Pinch to zoom and twist with two fingers to rotate on a touch screen; quarter turns snap, so a scan captured at an angle can be squared up. * **Marker colors are yours to choose.** Cool, Bright, and Deep suit different scan surfaces; Classic is the familiar green / red / yellow set, for the quickest separation between classes; and you can set your own three colors. Whichever you pick is a viewing preference stored on your device — it changes nothing that was measured, and no palette adds a ranking. Where the hues carry conventional meanings elsewhere, the viewer says so. * **The legend states that color is categorical**: it separates the marker classes and carries no severity. The viewer shows evidence. It offers no ranking, sorting, or highlighting that would steer you towards one reading. ## When the two scans can’t be compared [Section titled “When the two scans can’t be compared”](#when-the-two-scans-cant-be-compared) Before anything can be measured, the new scan and the Bind reference have to be brought into the same frame — aligned to each other. Sometimes they can’t be: too little of the surface was captured, the image is too blurred, or the scan is too poor to register. When that happens, DICE tells you the two scans could not be compared, and treats the measurements as not comparable. **This is not a statement about the identifier.** It says the comparison is unusable, nothing more, and it must not be read as either reassurance or concern. The honest thing to record in that situation is a Tamper Observation of **Unknown** — and, usually, to re-scan under better conditions. ## Recording a Tamper Observation [Section titled “Recording a Tamper Observation”](#recording-a-tamper-observation) A **Tamper Observation** is your conclusion, drawn from one Analysis, written in your name. It has exactly four possible results, and you choose one deliberately — there is no default and nothing is pre-selected: * **Consistent** — what you can see matches the reference captured at Bind; you see no evidence of tampering. * **Expected** — you see normal wear and tear, consistent with the identifier’s use case and substrate. An identifier that lives in the field accumulates honest degradation, and this result records exactly that without stretching either “consistent” or “inconsistent” to cover it. * **Inconsistent** — what you can see does not match the reference; you suspect tampering. * **Unknown** — the evidence does not support a conclusion, for example when the scans could not be compared or the scan is too poor to read. **Unknown carries the same weight as the other three.** It is the correct answer for unusable evidence, not an admission of defeat, and it is offered on equal footing so that nobody feels pushed into guessing. An Observation records the result, its author, and the time. Once written it is never edited or deleted, and it never replaces an earlier one — if you change your mind, or a colleague reads the same evidence differently, that is another Observation, and the Analysis keeps the whole series in order. What you see on an Analysis is therefore not “the answer” but the full record of who concluded what, and when. Two consequences worth planning around: * An Observation is about the **identifier surface** — whether its markers still match what was captured at Bind. It is not a statement about the Thread, and not a statement about the goods the Thread represents. Draw those conclusions yourself, outside the record, with everything else you know. * Because the series is permanent, an Observation written in the field stands as that person’s read, and an expert’s later Observation sits beside it rather than overwriting it. ## Who can do what [Section titled “Who can do what”](#who-can-do-what) Anyone who can view the Thread and its identifier can run a Tamper Analysis and record a Tamper Observation. There is no separate reviewer role. That is deliberate: the person holding the object is usually the only one who can see the thing being described, and locking authorship to a reviewer would leave the record carrying an opinion formed from a photograph. Attribution is what makes it safe — every Observation is permanently signed and dated, sits beside every other Observation on the same Analysis, and is never presented as DICE’s finding. Which reads to weigh, and whose, stays your organization’s decision. ## What downstream teams see [Section titled “What downstream teams see”](#what-downstream-teams-see) A Tamper Analysis and its Observations travel downstream exactly as far as the identifier they concern: * Where you have **disclosed** that identifier to a downstream team through [Fabric](/use/fabric/), they see its Analyses — the coverage measurements and whether the scans could be compared — together with the full Observation series and its authors. * Where the identifier is **withheld**, its Analyses and Observations do not appear downstream at all. Nothing is shown redacted, and nothing hints that an Analysis exists. There is no separate tamper visibility setting to manage: [disclosing](/use/disclosures/) the identifier is what carries its tamper record, and withholding the identifier is what keeps it back. The **evidence imagery does not travel.** A downstream reader gets the measurements and the Observations; the scan layers themselves stay with the team that captured them. ## Where Analyses appear afterwards [Section titled “Where Analyses appear afterwards”](#where-analyses-appear-afterwards) Analyses and Observations are recorded in the Thread’s transaction history alongside its bind, verify, and identify events, so a tamper record sits in the same timeline as everything else that happened to the item. See [Activity and transaction history](/use/activity/). If the evidence imagery for an older Analysis is no longer available, DICE says so, and the recorded measurements and Observations are unaffected. ## Related pages [Section titled “Related pages”](#related-pages) [Identifiers and scanning](/use/identifiers-and-scanning/)Bind, verify, and identify — where DUST scans come from. [Disclosures and updates](/use/disclosures/)What downstream teams see of an identifier, and therefore of its tamper record. [Activity](/use/activity/)The Thread timeline where Analyses and Observations are recorded. # Threads > Create, browse, edit, and archive threads — the digital records DICE keeps for your physical items. A **thread** is the digital record for one physical item. It holds the item’s data fields, files, identifiers, relationships to other threads, and a complete event history. As the empty state on the thread list puts it: “Each thread holds the identifiers, files, and history for one item.” Everything a Thread holds for one physical item. The same compartments appear as cards on the thread detail page. ## Browse and search threads [Section titled “Browse and search threads”](#browse-and-search-threads) Open **Search** in the sidebar to see your team’s threads. You can: * Search by name with the **Search threads…** box. * Toggle between **Table view** and **Cards view**. * Switch the page to **Files** to search files instead of threads (**Search Files**). ![The Search Threads page listing threads with their identifiers, folder, owner, and last update](/_astro/threads-search.D4JTl9H1_PHYkz.webp) The Search page lists your team's threads with their identifiers, folder, and owner. ### Filters [Section titled “Filters”](#filters) The **Thread filters** control offers: * Scope: **All threads**, **Created by me**, or **My Team**. * **Uncategorized** — threads not yet in any category. This scope only appears for organizations that have it enabled; see [Categories](/use/categories/). * **Include archived** — archived threads are hidden by default and marked with an **Archived** badge when shown. * **Transferred**: **Any**, **Shipped only**, or **Hide shipped** — threads that left your team in a shipment carry a **Shipped** badge. See [Shipments](/use/shipments/). * **Reset filters** to return to the defaults. Threads that other teams shared with you are not in this list — they live under **Shared** in the sidebar. See [Sharing and Access](/use/sharing-and-access/). ## Create a thread [Section titled “Create a thread”](#create-a-thread) Click **New Thread** in the sidebar to open **Create a New Thread**. Enter a **Name** (required), choose a **Folder** (required), optionally add a **Description**, and optionally pick a **Template**. Then fill in fields and click **Create Thread**. The full walkthrough is in [Getting started](/use/getting-started/). ## The thread detail page [Section titled “The thread detail page”](#the-thread-detail-page) ![A thread detail page showing data fields, an assembly membership, a bound QR identifier, and the transaction history](/_astro/thread-detail.GaWEUMeb_Z1v3Y6z.webp) A thread's detail page: fields, lineage, files, and the permanent transaction history — with banners for anything in flight, like a pending shipment. Opening a thread shows everything about it: * **Header** — the thread’s name, description, and thumbnail, with **Created** / **Updated** timestamps and creator. Actions include **Capture** (take a photo and attach it to the thread — on a phone this opens the camera, on a computer it opens a viewfinder in the page, with a camera picker when more than one camera is connected — and either way you review the photo and choose a full-quality or smaller upload before it is attached), **Share**, **Access**, and a **More actions** menu with **Edit thread**, **Mobile Link** (a QR code to open the page on a phone), **Move to Folder**, and **Archive** / **Restore**. * **Data** — the thread’s data fields (see below). * **Identifiers** — the identifiers bound to the thread, with a **Bind** button to add more. Click an identifier to view it, edit its name and description, **Verify** it, or **Unbind** it. See [Identifiers and Scanning](/use/identifiers-and-scanning/). * **Files** — grouped into **Images & Media** and **Documents & Files**, with **Upload**, search, and sorting (**Newest first**, **Oldest first**, **Name (A–Z)**, **Name (Z–A)**). Interrupted uploads can be resumed (**Resume Upload**). When a thread receives its first image — however it was added — DICE offers that image as the thread’s thumbnail, and you choose **Set Thumbnail** or **Skip**; nothing changes until you pick. * **Relationships** — “Related threads and dependencies”: links to other threads, with **Add** to create new links. See [Relationships](/use/relationships/). * **Part of** — “Where this thread is installed — outside its own contents”: the assemblies this thread is slotted into. See [Assemblies](/use/assemblies/). * **Transaction History** — the thread’s event feed, with **View events** to see everything that ever happened to it. See [Activity](/use/activity/). * **Viewing** — presence indicators showing who is looking at this thread right now (including **You**). ## Fields [Section titled “Fields”](#fields) ### Template fields and custom fields [Section titled “Template fields and custom fields”](#template-fields-and-custom-fields) * **Template fields** come from the template chosen at creation; they appear under a **Template fields** group and keep threads of the same kind consistent. * **Custom fields** are added per thread. In the data card, switch to **Edit** mode and use **Add field** — new fields are “Added to the end of the list”. You can also **Reorder** fields and **Save order**. ### Field types [Section titled “Field types”](#field-types) A field has a name, a type, and a value. Available types: Text, Long Text, Number, Boolean, Date, Date & Time, Duration, Time, Date Range, Tags, Select, Select Many, Email, Phone, URL, JSON, Audio, Document, File, Image, PDF, and Video. File-typed fields (Audio, Document, File, Image, PDF, Video) attach a file from the thread or a new upload. ### Required and private fields [Section titled “Required and private fields”](#required-and-private-fields) When editing a field you can mark it: * **Required** — “Make this field required”. * **Private** — “Hide this field from shared views”. Private field values stay visible to the owning team but are not exposed to teams the thread is shared with. ### Archiving and restoring field data [Section titled “Archiving and restoring field data”](#archiving-and-restoring-field-data) Deleting a field archives it rather than destroying it. The data card has an **Archived fields** section where each archived field shows when it was archived and offers **Restore**. Private archived fields keep their **Private** marker. ## Files and identifiers can be archived and made private too [Section titled “Files and identifiers can be archived and made private too”](#files-and-identifiers-can-be-archived-and-made-private-too) From a file’s actions menu you can: * **Set as thumbnail** — use an image as the thread’s thumbnail. * **Make private** / **Make public** — private files are hidden from shared views. “Files can only be made private from the thread owner organization.” * **Archive file** / **Restore file** — archived files move to an **Archived files** list on the thread, where each entry shows its archive date and a **Restore** button. * **Associate with field** — bind the file to a data field, or create a new image field for it. * **Download**. Identifiers can likewise be unbound from the identifier dialog (**Unbind** — “This will remove the identifier from this thread. This action cannot be undone.”). ## Edit a thread [Section titled “Edit a thread”](#edit-a-thread) Choose **Edit thread** from the thread’s **More actions** menu to open the **Edit Thread** dialog — “Update the thread name and description.” — then **Save**. Field values are edited directly in the data card’s **Edit** mode. ## Archive and restore a thread [Section titled “Archive and restore a thread”](#archive-and-restore-a-thread) 1. On the thread page, open **More actions** and choose **Archive**. 2. The thread now shows a banner: “This thread is archived.” with the explanation “Archived threads are hidden from thread lists and search by default. The thread is not deleted; its details, files, and links are preserved and it can be restored.” 3. To bring it back, open the archived thread (use the **Include archived** filter to find it) and choose **Restore**. A “Thread restored” confirmation appears. ## Templates [Section titled “Templates”](#templates) Templates let you reuse field definitions across many threads. * **Using a template**: pick one in the **Template** dropdown when creating a thread; its fields appear pre-populated under **Thread Fields**. If a selected template contains field types your app can’t render, the form warns you — you can still create the thread without it. * **Managing templates**: open **Thread Templates** in the sidebar. Search by name or description, or click **New Template** to open **Create a New Template** — “Templates let you reuse thread field definitions across multiple threads.” The empty state invites you to “Create your first template”. Templates are also created automatically when you import threads from a CSV; see [Import](/use/import/). ## Related pages [Section titled “Related pages”](#related-pages) * Shipped and locked threads (shipment banners on the thread page): [Shipments](/use/shipments/) * Deriving new threads from an existing one: [Slicing](/use/slicing/) * Reviewing updates disclosed by a thread’s sources: [Disclosures](/use/disclosures/) * Thread lineage and provenance: [Fabric](/use/fabric/) # Vlinks > Reserve and manage permanent redirect links and QR codes, individually or in collections. A **Vlink** is a permanent public URL that redirects to a destination you control. Its printed URL and QR code stay the same when the destination’s content changes, making it suitable for physical products, certificates, packaging, and product documentation. Note Vlinks availability depends on your organization’s configuration. The **Vlinks** entry appears only for an admin of the active Team when the module is enabled. Certificate Forms can still create the Vlinks required by their QR zones when the standalone module is not available. ## Reserve a Vlink [Section titled “Reserve a Vlink”](#reserve-a-vlink) 1. Open **Vlinks** from the sidebar and choose **Create vlink**. 2. Optionally name a collection. Leave the collection blank to place the link under **Unfiled vlinks**. 3. Choose how many links to create. A single operation can reserve up to **50 Vlinks** in the same collection. 4. Leave the destination blank to reserve permanent URLs as drafts, or enter an HTTP(S) destination and create active links immediately. Each Vlink has its own stable URL. From its details you can copy the link, preview its QR code, download the QR code, or print it. ## Collections [Section titled “Collections”](#collections) The overview groups Vlinks into named collections plus **Unfiled vlinks**. Collection cards show the number of draft, active, inactive, and revoked links. Open a collection to search the visible links, move through paginated results, inspect destinations, and change link status. Collections organize inventory only. Moving or renaming the surrounding business workflow does not change a Vlink’s permanent URL. ## Status and destination [Section titled “Status and destination”](#status-and-destination) | Status | Scan behavior | | ------------ | ----------------------------------------------------------------------------------- | | **Draft** | The URL is reserved but does not redirect. | | **Active** | The URL redirects to its destination. | | **Inactive** | The URL uses its fallback destination when one exists; otherwise it is unavailable. | | **Revoked** | The URL is permanently unavailable. | An active Vlink must have a destination. You can deactivate and reactivate an ordinary Vlink as needed. Revocation is terminal: Vlinks are retained as records rather than deleted or reused. ## Vlinks printed on Certificates [Section titled “Vlinks printed on Certificates”](#vlinks-printed-on-certificates) A Certificate Form can contain one or more **Vlink URL** QR zones. During Certificate generation, each zone receives an independent Vlink and destination choice: the Thread’s Public Page, a custom URL, or a draft to configure later. Once a Certificate Vlink is active, its destination is locked so the meaning of the already-printed QR code cannot silently change. Its status can still be changed independently. A Certificate Vlink left as a draft may be given its first destination and activated from its Vlink details. ## Analytics [Section titled “Analytics”](#analytics) Vlink details show privacy-safe visit totals and activity over time, including the first and most recent visit. These metrics help confirm that a printed link is being used without changing its permanent URL. ## Related pages [Section titled “Related pages”](#related-pages) * [Certificates](/use/certificates/) — place independent Vlink QR zones on immutable PDFs * [Public Pages](/use/public-pages/) — publish the product page a Vlink can redirect to * [Administrator guide](/use/admin-guide/) — roles and module availability