Skip to content

Error Handling

The Ditio API uses standard HTTP status codes. This page covers common errors and how to handle them.

StatusMeaningWhat to do
200 OKRequest succeededProcess the response data
201 CreatedResource created successfullyApplies to POST endpoints
204 No ContentSuccess, no response bodyApplies to DELETE and some PATCH endpoints
400 Bad RequestInvalid request parameters or validation errorCheck your request body or query parameters
401 UnauthorizedToken missing, expired, or issued for a different scope than the endpoint requiresCheck the scope you asked for at the token endpoint, then refresh — see Authentication
403 ForbiddenToken is valid and accepted, but the client may not touch this dataCheck whether the project/company belongs to your client, and whether the integration is enabled for it
404 Not FoundEndpoint or resource doesn’t existCheck the URL and resource ID
429 Too Many RequestsRate limit exceededWait and retry with exponential backoff
500 Internal Server ErrorServer-side errorRetry after a short delay. Contact support if persistent

Simple errors include a message explaining what went wrong:

{
"error": "ArgumentIsNull",
"message": "The 'ModifiedSince' parameter is required."
}

Validation errors from the v5 Integration API carry a structured errors array:

{
"message": "Validation failed",
"errors": [
{
"category": "UserInvalidInputError",
"code": "ValidationError",
"field": "Phone",
"message": "The Phone field is required."
}
]
}

Each API validates the scope as the token’s audience, so the token is rejected during authentication rather than authorisation. That means you get 401, and the usual 401 advice — “refresh the token” — will loop forever, because the fresh token has the same wrong scope as the old one.

Read the WWW-Authenticate response header to tell the cases apart:

WWW-AuthenticateMeaningFix
Bearer error="invalid_token", error_description="The audience '<scope>' is invalid"Wrong scope — <scope> lists the scope(s) your token was issued forRe-request the token with the scope this endpoint needs
Bearer error="invalid_token", error_description="The issuer '<host>' is invalid"The token came from the other environment — e.g. an identity.ditio.dev token sent to core-api.ditio.appFetch the token from the identity host that matches your API host
Bearer error="invalid_token", error_description="The token expired at '<timestamp>'"Expired tokenRefresh the token
Bearer error="invalid_token" (no description)Malformed or unparseable tokenCheck you are sending the access_token value, unmodified
Bearer (no error)No Authorization header sentSend Authorization: Bearer <token>

The fix is always at the token endpoint, never on the API request — scope is a property of the token, and Ditio APIs ignore a Scope header on the request itself:

Terminal window
# Wrong — the API request carries a scope header, the token doesn't have the scope
curl -H "Authorization: Bearer $TOKEN" -H "Scope: reportingapiv1" ...
# Right — ask for the scope when you fetch the token
curl -X POST "$DITIO_IDENTITY_BASE/connect/token" \
-d "grant_type=client_credentials" \
-d "client_id=$CLIENT_ID" \
-d "client_secret=$CLIENT_SECRET" \
-d "scope=reportingapiv1"

See Which scope for which host for the scope each base URL expects.

Access tokens are short-lived (about 30 minutes by default). When one expires, request a new token and retry:

if response.status_code == 401:
token = refresh_access_token()
response = requests.get(url, headers={"Authorization": f"Bearer {token}"})

If a retry with a brand-new token returns 401 again, stop retrying — the token is being rejected for something other than expiry. Read WWW-Authenticate to see which: wrong scope, wrong environment, or a malformed token.

403 Forbidden — wrong company or integration not enabled

Section titled “403 Forbidden — wrong company or integration not enabled”

A 403 means the token was accepted — right scope, valid signature, not expired — but the client isn’t allowed to do what you asked. Common causes:

  • The project or company doesn’t belong to your client.
  • The integration isn’t enabled for that company or project.
  • The API client is marked read-only and the request tried to write. This affects writes only; reads and extraction calls still work.

Requesting a different scope will not fix a 403.

400 invalid_scope — scope not granted to your client

Section titled “400 invalid_scope — scope not granted to your client”

This one comes from the token endpoint, not from an API call:

{ "error": "invalid_scope" }

It means your client isn’t granted one of the scopes you asked for. The request is all-or-nothing — the identity server issues no token at all rather than returning the scopes you are allowed. Ask only for scopes your client has; reportingapiv1 in particular cannot be enabled self-service, so contact support@ditio.no if you need it added.

Integration API DELETE endpoints refuse to delete resources with dependent data (e.g. a project with time registrations). Deactivate instead (active: false), or use the is-*-deletable / deletable check endpoints first.

400 Bad Request — invalid incremental-sync window

Section titled “400 Bad Request — invalid incremental-sync window”

Extraction endpoints validate the sync window: ModifiedBefore requires ModifiedSince and must be strictly after it.

For transient errors (429, 500, 502, 503, 504), implement exponential backoff:

  1. Wait 1 second, retry
  2. Wait 2 seconds, retry
  3. Wait 4 seconds, retry
  4. Wait 8 seconds, retry
  5. Give up and log the error
import time
import requests
def api_call_with_retry(url, headers, max_retries=4):
for attempt in range(max_retries + 1):
response = requests.get(url, headers=headers)
if response.status_code in (429, 500, 502, 503, 504):
if attempt < max_retries:
time.sleep(2 ** attempt)
continue
return response
return response

Don’t retry 400/404 — those indicate a problem with the request itself.