Record provenance from your ERP
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.
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 and attributing every entry to the human operator who acted in your system.
The flow at a glance
Section titled “The flow at a glance”- Your integration exchanges its Service Account credential for a short-lived bearer token (Authentication).
- Something happens in your system — a goods issue, an inspection, a repair closure.
- Your integration calls
POST /api/v1/events/declarewith the Thread id, a headline, and the claim’s time and place, sending the operator’s identity in theDust-Ctx-Declared-Actorheader. - 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”- A Service Account with an API key or OAuth client — see 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-Idheader, and the Team id if the Service Account should act as a specific Team — see Request 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).
Declare a transaction
Section titled “Declare a transaction”One call records one entry on one Thread:
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" } }'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”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.
Attribute the human operator
Section titled “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:
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.
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”When one business event touches many items — a goods receipt of 200 serialized units, a lot-level inspection — declare once across all of them:
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" } }'threadIdstakes 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).
Backfill many entries at once
Section titled “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:
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
anchorand no evidenceresIds— both are single-claim gestures. Use/declarefor those. /declare/batchis 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 instead of building an integration.
Correct a mistake
Section titled “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:
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.
Failure modes
Section titled “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.
Next steps
Section titled “Next steps”- Authentication and API keys — Service Accounts, token exchange, declared actor semantics.
- Threads API guide — resolving your system’s serials and orders to Thread ids.
- Recording past events — the same feature as your operators see it in DICE.
- Public Pages — how declared entries appear on the item’s public passport.
