Skip to content

Authentication and API keys

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 <token> on API calls, and re-exchange when the token expires.

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.

Service Accounts and their credentials are managed by organization admins in the AuthD portal at authd.dustid.io.

  1. Sign in at 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.

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.)

Terminal window
curl -fsS "https://apid.dustid.io/api/auth/token" \
-H "x-api-key: $DUST_API_KEY"

Response:

{ "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.

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:

Terminal window
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):

{ "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.

Send the token on every core API call:

Authorization: Bearer <token>

A quick way to confirm the token works:

Terminal window
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 for the error contract.

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):

let cached: { token: string; refreshAfter: number } | null = null;
async function getToken(): Promise<string> {
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<Response> {
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.

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:

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.

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:

GET https://apid.dustid.io/api/auth/jwks

It returns a standard { "keys": [ ... ] } document usable with any JOSE library.

  • 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.

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.