Authentication
All Ditio API endpoints require authentication. The API uses OAuth 2.0
client credentials: you exchange a client_id and client_secret for a
short-lived bearer token, and send that token with every request.
Environment variables
Section titled “Environment variables”Ditio has a test and a production environment, each with its own hosts and its own credentials: a client created in production does not work in test, and vice versa. The hosts for every API are listed on Environments.
All code samples in this documentation use environment variables so you can switch environments in one place — and they default to test:
# Test (default for all examples)export DITIO_API_BASE="https://core-api.ditio.dev/core"export DITIO_REPORTING_BASE="https://core-api.ditio.dev/reporting"export DITIO_MASSTRANSPORT_BASE="https://core-api.ditio.dev/masstransport"export DITIO_IDENTITY_BASE="https://identity.ditio.dev"
# Production — only after verifying in test# export DITIO_API_BASE="https://integration.ditio.no"# export DITIO_REPORTING_BASE="https://integration.ditio.no/reporting"# export DITIO_MASSTRANSPORT_BASE="https://core-api.ditio.app/masstransport"# export DITIO_IDENTITY_BASE="https://identity.ditio.app"
# Your API client credentials (see Creating API credentials below)export DITIO_CLIENT_ID="your-client-id"read -rs DITIO_CLIENT_SECRET && export DITIO_CLIENT_SECRET # prompts without echo; keeps the secret out of shell historyThe scope each base URL needs is in Which scope for which host.
Creating API credentials
Section titled “Creating API credentials”Any Ditio administrator can create API credentials self-service:
-
Log in to Ditio Web.
-
Go to Company Setup (Oppsett) → Integration.
-
Click Create new API client and give it a descriptive name (e.g. “ERP Integration”, “Power BI”).
-
The system generates a
client_idandclient_secret.
Create separate clients for separate use cases so they can be disabled or deleted independently. Disabling a client immediately revokes its access.
When a client stops working
Section titled “When a client stops working”Ditio checks the API client on every call, not only when it issues the token. A client stops working when:
- an administrator disables or deletes it;
- it passes its expiry date. A client is valid for three years from creation unless a different expiry date was set when it was created;
- the user the client acts as is disabled or removed.
Every call then returns 401 with a JSON body whose message says the
client “either does not exist or is disabled”, or that its emulation user
“is not found or disabled”. A new token doesn’t help, so don’t retry. An
administrator has to re-enable the client or its user in Ditio Web, or
create a new client.
Obtaining a Token
Section titled “Obtaining a Token”Exchange your credentials for a bearer token at the identity server:
curl -X POST "$DITIO_IDENTITY_BASE/connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=YOUR_CLIENT_ID" \ -d "client_secret=YOUR_CLIENT_SECRET" \ -d "scope=ditioapiv3"Response:
{ "access_token": "eyJhbGciOiJSUzI1NiIs...", "expires_in": 1800, "token_type": "Bearer", "scope": "ditioapiv3"}The scope you request decides which API the token works against, so it has
to match the endpoint you are calling — see Which scope for which
host below. ditioapiv3 covers the Core and
Integration API; the Data Extraction API needs reportingapiv1 instead.
Getting a token in your scripts
Section titled “Getting a token in your scripts”The examples in the guides assume you already have a token: $TOKEN in shell
examples and get_token(...) in Python examples. Both come from here; pass the
scope the guide names (in shell, set DITIO_SCOPE to it, e.g.
DITIO_SCOPE=reportingapiv1). Credentials default to DITIO_CLIENT_ID /
DITIO_CLIENT_SECRET; pass them explicitly if you hold one client per customer.
# Store the token in $TOKEN (requires jq). Set DITIO_SCOPE to the scope the guide names.TOKEN=$(curl -s -X POST "$DITIO_IDENTITY_BASE/connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=$DITIO_CLIENT_ID" \ -d "client_secret=$DITIO_CLIENT_SECRET" \ -d "scope=${DITIO_SCOPE:-ditioapiv3}" | jq -r '.access_token')import osimport requests
IDENTITY_BASE = os.environ.get("DITIO_IDENTITY_BASE", "https://identity.ditio.dev")
def get_token(scope: str, client_id=None, client_secret=None) -> str: response = requests.post(f"{IDENTITY_BASE}/connect/token", data={ "grant_type": "client_credentials", "client_id": client_id or os.environ["DITIO_CLIENT_ID"], "client_secret": client_secret or os.environ["DITIO_CLIENT_SECRET"], "scope": scope, }, timeout=30) response.raise_for_status() return response.json()["access_token"]In C#, use DitioAuth.GetTokenAsync(http, scope) from Token
Expiry, which also caches one token per client and scope.
Scopes
Section titled “Scopes”| Scope | Grants access to |
|---|---|
ditioapiv3 | Core + Integration API — use this for most integrations |
masstransportapiv1 | Mass Transport API (machine integration) |
reportingapiv1 | Reporting / Data Extraction API |
If your integration calls more than one of these APIs, you can request several scopes in one token by separating them with a space:
-d "scope=ditioapiv3 reportingapiv1"Which scope for which host
Section titled “Which scope for which host”Each base URL requires its own scope. A token is accepted only if it carries
the scope for the API you are calling — a token requested with ditioapiv3
alone is not accepted by $DITIO_REPORTING_BASE, even where the two
share a hostname. A token requested with several scopes carries all of
them and is accepted by every host whose scope it holds.
| Base URL variable | Scope required | Documented in |
|---|---|---|
$DITIO_API_BASE | ditioapiv3 | Integration API |
$DITIO_REPORTING_BASE | reportingapiv1 | Data Extraction API |
$DITIO_MASSTRANSPORT_BASE | masstransportapiv1 | Machine Integration, Weighbridge Integration |
Using the Token
Section titled “Using the Token”Include the access token in the Authorization header of every API request:
curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ "$DITIO_API_BASE/api/v4/integration/projects"Scope is never read from a request header — it comes from the token.
Token Expiry
Section titled “Token Expiry”Tokens are short-lived — about 30 minutes by default (configurable per
client, up to 24 hours). Read expires_in from the token response, cache the
token, and refresh it shortly before it lapses. Your integration should also
handle 401 Unauthorized responses by requesting a new token — but only
retry once. If the retry also returns 401, the token is being rejected
for something other than expiry and retrying will loop forever; read the
WWW-Authenticate header to see which cause it is, per Error
Handling. Never
request a fresh token per API call.
Paste the DitioAuth class at the end of Program.cs (after your top-level
statements) or into its own file, and put the using lines at the top of that
file. It keeps one cached token per client_id and scope, so a system serving
several Ditio customers never reuses one customer’s token for another. On a
401, call it once with forceRefresh: true and retry the request once — if
that retry also returns 401, stop and read WWW-Authenticate as described
above.
// at the top of the fileusing System.Collections.Concurrent;using System.Net.Http.Json;
static class DitioAuth{ // One cached token per (client, scope); only fetch a new one shortly before it expires. // Concurrent so parallel requests can share the cache safely. private static readonly ConcurrentDictionary<(string? ClientId, string Scope), (string Token, DateTime ExpiresUtc)> Cache = new();
// clientId/clientSecret default to DITIO_CLIENT_ID/DITIO_CLIENT_SECRET. public static async Task<string> GetTokenAsync( HttpClient http, string scope = "ditioapiv3", string? clientId = null, string? clientSecret = null, bool forceRefresh = false) { clientId ??= Environment.GetEnvironmentVariable("DITIO_CLIENT_ID")!; clientSecret ??= Environment.GetEnvironmentVariable("DITIO_CLIENT_SECRET")!; var key = (clientId, scope);
// Refresh ~1 minute early so a token never expires mid-request. if (!forceRefresh && Cache.TryGetValue(key, out var cached) && DateTime.UtcNow < cached.ExpiresUtc.AddMinutes(-1)) return cached.Token;
var identityBase = Environment.GetEnvironmentVariable("DITIO_IDENTITY_BASE") ?? "https://identity.ditio.dev"; var response = await http.PostAsync( $"{identityBase}/connect/token", new FormUrlEncodedContent(new Dictionary<string, string> { ["grant_type"] = "client_credentials", ["client_id"] = clientId, ["client_secret"] = clientSecret, // Must match the API being called — see "Which scope for which host". ["scope"] = scope, })); response.EnsureSuccessStatusCode();
var token = await response.Content.ReadFromJsonAsync<TokenResponse>(); Cache[key] = (token!.access_token, DateTime.UtcNow.AddSeconds(token.expires_in)); return token.access_token; }
private record TokenResponse(string access_token, int expires_in, string token_type);}Long-lived tokens from the UI
Section titled “Long-lived tokens from the UI”If you can’t run the token flow (e.g. a BI tool that only accepts a static token), an administrator can generate a longer-lived token directly in the Ditio Web UI on the integration page (these are audited). Each API client can be disabled or deleted at any time from the same page, immediately revoking access.
Security Best Practices
Section titled “Security Best Practices”- Never expose your Client Secret in client-side code or version control
- Store credentials in environment variables or a secrets manager
- Rotate your Client Secret periodically
- Use the minimum required scopes for your integration
- Implement token caching to avoid unnecessary token requests