Skip to content

API quickstart

By the end of this page you will have exchanged an API key for a bearer token, discovered the organization and Team your credential can act in, created a Thread, and read it back with its event history.

Both language paths below are complete and independent: everything is defined before it is used, and neither borrows a step from the other. Pick one tab and stay in it.

You need:

  • A Service Account API key, issued by an organization admin. Personal API keys do not exist — see Authentication and API keys if you do not have one yet.
  • The Service Account must be a member of the Team you write into. Creating Threads requires current membership in the selected Team; an organization-level credential that belongs to no Team can read /api/v1/me but cannot create records. Ask your admin to add it to a Team if step 3 returns 403 FORBIDDEN.
  • curl path: curl and jq (the examples parse JSON with it; if you would rather not install jq, copy the values out of the responses by hand).
  • TypeScript path: a runtime that runs TypeScript directly and has a global fetch — Node.js 22.18 or newer, Bun, or Deno. On Node.js 18 or 20, run the file with a loader such as tsx instead. No packages to install: the examples use plain fetch only. A typed client is available separately, see TypeScript client.

All requests go to https://apid.dustid.io; see Environments for the other service URLs.

  1. Terminal window
    export APID_URL="https://apid.dustid.io"
    export DUST_API_KEY="your-service-account-key" # read this from your secrets manager
  2. API keys are never sent to /api/v1/* endpoints. Exchange the key once at GET /api/auth/token, passing it in the x-api-key header, and send the resulting JWT as Authorization: Bearer <token> on every subsequent call.

    Terminal window
    curl -fsS "$APID_URL/api/auth/token" -H "x-api-key: $DUST_API_KEY"
    { "token": "eyJhbGciOi...", "expiresIn": 900, "expiresAt": "2026-09-20T22:40:00.000Z" }
    Terminal window
    export DUST_TOKEN="$(
    curl -fsS "$APID_URL/api/auth/token" -H "x-api-key: $DUST_API_KEY" | jq -r '.token'
    )"

    The response carries token, and — whenever the JWT itself has an expiry claim — expiresIn (seconds remaining) and expiresAt (ISO 8601). Read the lifetime from the response rather than hardcoding one: tokens are currently short-lived (about 15 minutes) and there is no refresh token, so a long-running job must re-exchange mid-run. The full lifetime contract, a caching implementation, and the refresh-once-on-401 pattern are in Authentication → Token expiry and refresh.

  3. GET /api/v1/me is one of the few endpoints that needs no context headers. It describes the credential itself: the principal, the organizations it belongs to, and which one is active.

    Terminal window
    curl -fsS "$APID_URL/api/v1/me" -H "Authorization: Bearer $DUST_TOKEN"
    {
    "userId": "6a1f…",
    "email": "sap-connector@example.com",
    "name": "SAP Connector",
    "activeOrganizationId": "b2c7…",
    "organizations": [
    { "id": "b2c7…", "name": "Anchor Electronics", "slug": "anchor-electronics", "roles": ["member"] }
    ]
    }
    Terminal window
    # Prefer the active organization; fall back to the first membership.
    export DUST_ORG_ID="$(
    curl -fsS "$APID_URL/api/v1/me" -H "Authorization: Bearer $DUST_TOKEN" \
    | jq -er '.activeOrganizationId // .organizations[0].id'
    )"
    echo "Organization: $DUST_ORG_ID"
  4. Records belong to a Team inside the organization. You have two supported options:

    • Do nothing. Omit Dust-Ctx-Team-Id and the API acts in the organization’s root Team. That is the whole of step 4 for a single-Team organization, and the examples in step 5 take this path.
    • Name a Team. GET /api/v1/teams lists the Teams your credential is a member of, as { "teams": [ … ], "total": n }, each with teamId, orgId, and name. Send the one you want as Dust-Ctx-Team-Id.
    Terminal window
    curl -fsS "$APID_URL/api/v1/teams?pageSize=50" \
    -H "Authorization: Bearer $DUST_TOKEN" \
    -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \
    | jq '.teams[] | { teamId, name }'
    { "teamId": "b2c7…", "name": "Anchor Electronics" }
    { "teamId": "4e90…", "name": "Line 3 Receiving" }
    Terminal window
    # Optional. Leave DUST_TEAM_ID unset to use the organization's root Team.
    export DUST_TEAM_ID="4e90…"

    Every request below passes -H "Dust-Ctx-Team-Id: ${DUST_TEAM_ID:-}". An empty value is treated exactly like an absent header — the organization’s root Team — so the same script runs whether or not you set the variable.

  5. A Thread is the record for one asset or item. POST /api/v1/threads with type: "single" creates one; thread.name is the only required field, and the optional data array carries typed fields.

    Terminal window
    curl -fsS "$APID_URL/api/v1/threads" \
    -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 '{
    "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 } }
    ]
    }' | tee /tmp/created.json | jq '.created[0] | { threadId, name }'
    { "threadId": "0f13c0de-2f1a-4a2e-9f60-6d2f7b9f0a11", "name": "Tire SZ3J-11-ZJ17" }
    Terminal window
    export THREAD_ID="$(jq -r '.created[0].threadId' /tmp/created.json)"

    The status is 201 Created and the body is { "created": [ … ], "uploadResponses": [] } — a batch shape, because the same endpoint creates many Threads at once with type: "list" or type: "raw". Each entry in created is a full Thread record including its generated threadId.

    Field entries need type and value, and the shape of value follows the type: { "text": "…" } for text, { "number": 51 } for number. name is the field’s label. The full list of field types is in the Threads guide.

  6. GET /api/v1/threads/{thread_id} returns the Thread plus its event history — every write is recorded, so the audit trail starts at creation.

    Terminal window
    curl -fsS "$APID_URL/api/v1/threads/$THREAD_ID" \
    -H "Authorization: Bearer $DUST_TOKEN" \
    -H "Dust-Ctx-Org-Id: $DUST_ORG_ID" \
    -H "Dust-Ctx-Team-Id: ${DUST_TEAM_ID:-}" \
    | jq '{ name: .thread.name, fields: [.thread.data[]?.name], events: (.events | length) }'
    { "name": "Tire SZ3J-11-ZJ17", "fields": ["Serial Number", "Max PSI"], "events": 1 }

    The response shape is { "thread": { … }, "events": [ … ] }. The exact event count is not a contract — expect at least one.

What you sawWhat it means
401 UNAUTHORIZED on the exchangeThe API key is wrong, revoked, or not a Service Account key. Personal keys do not authenticate.
401 UNAUTHORIZED on a /api/v1/* callThe bearer token expired (they are short-lived). Re-exchange and retry once.
400 ORG_ID_REQUIREDYou omitted Dust-Ctx-Org-Id on an org-scoped endpoint.
400 INVALID_REQUEST naming a headerA context header was not a UUID. Headers are validated before the endpoint runs.
403 FORBIDDEN on createThe Service Account is not a member of the selected Team. Ask your admin to add it.
404 on the read-backUsually the wrong context, not a missing record — a Thread is only visible in the organization and Team that own or were shared it.

Every error body is { code, message, status, detail? }, and every response carries an x-request-id header worth logging. The complete code list, the scanning outcome tables, and retry guidance are in Errors and scan outcomes.

  • Request conventions — context headers, pagination, localization.
  • Errors and scan outcomes — the failure contract in full.
  • Threads — field types, updates, archiving, listing and search.
  • Identifiers — bind and verify physical identifiers against Threads (the /api/v1/tags/* endpoints).
  • Files — attach evidence files to Threads.
  • Teams and sharing — cross-Team access.
  • TypeScript client — a typed alternative to raw fetch.
  • Full API reference — every endpoint, generated from the OpenAPI spec. The API server also self-hosts an interactive reference at https://apid.dustid.io/api/docs and the raw spec at /api/openapi.json.