Quickstart
This guide takes you from credentials to working API calls: first a token, then creating a project and a work order (the write surface), then pulling time registrations (the read surface).
Prerequisites
Section titled “Prerequisites”- API credentials (
client_idandclient_secret) — see Authentication - A tool for making HTTP requests (
curl, Postman, or your language of choice). The bash examples also usejqto read JSON responses
# Test (default for all examples)export DITIO_IDENTITY_BASE="https://identity.ditio.dev"export DITIO_API_BASE="https://core-api.ditio.dev/core"export DITIO_REPORTING_BASE="https://core-api.ditio.dev/reporting"The four calls
Section titled “The four calls”-
Get an access token for the Integration API, with scope
ditioapiv3. Steps 2 and 3 use it.Terminal window 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=YOUR_CLIENT_ID" \-d "client_secret=YOUR_CLIENT_SECRET" \-d "scope=ditioapiv3" | jq -r '.access_token') -
Create a project. The response contains the project’s generated
id— you need it in the next step.Terminal window curl -X POST "$DITIO_API_BASE/api/v4/integration/projects" \-H "Authorization: Bearer $TOKEN" \-H "Content-Type: application/json" \-d '{"companyId": "YOUR_COMPANY_ID","projectNumber": "P-1001","name": "Prosjekt Fjellhall","active": true}' -
Create a work order in that project. That’s the minimum working setup: a project with a work order that employees can register time against.
Terminal window curl -X POST "$DITIO_API_BASE/api/v4/integration/tasks" \-H "Authorization: Bearer $TOKEN" \-H "Content-Type: application/json" \-d '{"companyId": "YOUR_COMPANY_ID","projectId": "PROJECT_ID_FROM_STEP_2","externalId": "WO-001","name": "Graving byggegrop","active": true}' -
Pull time registrations. Once time has been registered (in test there’s usually existing synced data), export it through the Data Extraction API. That API needs its own token, with scope
reportingapiv1.If the token request returns
400/invalid_scope, your client isn’t grantedreportingapiv1— ask [email protected] to enable it. See Authentication.Terminal window REPORTING_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=YOUR_CLIENT_ID" \-d "client_secret=YOUR_CLIENT_SECRET" \-d "scope=reportingapiv1" | jq -r '.access_token')curl -H "Authorization: Bearer $REPORTING_TOKEN" \"$DITIO_REPORTING_BASE/v1/time-registrations?FromDateTime=2026-01-01&ToDateTime=2026-01-31"
The extraction response contains a data array and — when there is more
data — a continuationToken (the full response envelope is in
Pagination):
{ "data": [], "links": { "next": "..." }, "continuationToken": "eyJ..."}Pass the token back as ContinuationToken to get the next page, and repeat
until it’s empty. See Pagination.
Python example
Section titled “Python example”import osimport requests
IDENTITY_BASE = os.environ.get("DITIO_IDENTITY_BASE", "https://identity.ditio.dev")API_BASE = os.environ.get("DITIO_API_BASE", "https://core-api.ditio.dev/core")REPORTING_BASE = os.environ.get("DITIO_REPORTING_BASE", "https://core-api.ditio.dev/reporting")
def get_token(scope): response = requests.post( f"{IDENTITY_BASE}/connect/token", data={ "grant_type": "client_credentials", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "scope": scope, }, timeout=30, ) response.raise_for_status() return response.json()["access_token"]
# Step 1: authenticate for the Integration APIheaders = {"Authorization": f"Bearer {get_token('ditioapiv3')}"}
# Step 2: create a projectresponse = requests.post( f"{API_BASE}/api/v4/integration/projects", headers=headers, json={ "companyId": "YOUR_COMPANY_ID", "projectNumber": "P-1001", "name": "Prosjekt Fjellhall", "active": True, }, timeout=30,)response.raise_for_status()project = response.json()
# Step 3: create a work order in itrequests.post( f"{API_BASE}/api/v4/integration/tasks", headers=headers, json={ "companyId": "YOUR_COMPANY_ID", "projectId": project["id"], "externalId": "WO-001", "name": "Graving byggegrop", "active": True, }, timeout=30,).raise_for_status()
# Step 4: the Data Extraction API needs its own token, with scope reportingapiv1reporting_headers = {"Authorization": f"Bearer {get_token('reportingapiv1')}"}
# Pull time registrations, following paginationall_records = []params = {"FromDateTime": "2026-01-01", "ToDateTime": "2026-01-31"}while True: response = requests.get( f"{REPORTING_BASE}/v1/time-registrations", headers=reporting_headers, params=params, timeout=30, ) response.raise_for_status() page = response.json() all_records.extend(page["data"]) continuation = page.get("continuationToken") if not continuation: break params["ContinuationToken"] = continuation
print(f"Fetched {len(all_records)} time registrations")C# example
Section titled “C# example”The same four steps as a .NET console app (Program.cs):
using System.Net.Http.Headers;using System.Net.Http.Json;using System.Text.Json;
var identityBase = Environment.GetEnvironmentVariable("DITIO_IDENTITY_BASE") ?? "https://identity.ditio.dev";var apiBase = Environment.GetEnvironmentVariable("DITIO_API_BASE") ?? "https://core-api.ditio.dev/core";var reportingBase = Environment.GetEnvironmentVariable("DITIO_REPORTING_BASE") ?? "https://core-api.ditio.dev/reporting";
using var http = new HttpClient();
async Task<string> GetTokenAsync(string scope){ var response = await http.PostAsync( $"{identityBase}/connect/token", new FormUrlEncodedContent(new Dictionary<string, string> { ["grant_type"] = "client_credentials", ["client_id"] = "YOUR_CLIENT_ID", ["client_secret"] = "YOUR_CLIENT_SECRET", ["scope"] = scope, })); response.EnsureSuccessStatusCode(); return (await response.Content.ReadFromJsonAsync<TokenResponse>())!.access_token;}
// Step 1: authenticate for the Integration APIvar apiToken = await GetTokenAsync("ditioapiv3");http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiToken);
// Step 2: create a projectvar projectResponse = await http.PostAsJsonAsync( $"{apiBase}/api/v4/integration/projects", new { companyId = "YOUR_COMPANY_ID", projectNumber = "P-1001", name = "Prosjekt Fjellhall", active = true, });projectResponse.EnsureSuccessStatusCode();var project = await projectResponse.Content.ReadFromJsonAsync<JsonElement>();var projectId = project.GetProperty("id").GetString();
// Step 3: create a work order in itvar taskResponse = await http.PostAsJsonAsync( $"{apiBase}/api/v4/integration/tasks", new { companyId = "YOUR_COMPANY_ID", projectId, externalId = "WO-001", name = "Graving byggegrop", active = true, });taskResponse.EnsureSuccessStatusCode();
// Step 4: the Data Extraction API needs its own token, with scope reportingapiv1var reportingToken = await GetTokenAsync("reportingapiv1");http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", reportingToken);
// Pull time registrations, following paginationvar url = $"{reportingBase}/v1/time-registrations?FromDateTime=2026-01-01&ToDateTime=2026-01-31";var recordCount = 0;string? continuationToken = null;do{ var pageUrl = continuationToken is null ? url : $"{url}&ContinuationToken={Uri.EscapeDataString(continuationToken)}"; var page = await http.GetFromJsonAsync<JsonElement>(pageUrl); recordCount += page.GetProperty("data").GetArrayLength(); continuationToken = page.TryGetProperty("continuationToken", out var next) && next.ValueKind == JsonValueKind.String ? next.GetString() : null;} while (!string.IsNullOrEmpty(continuationToken));
Console.WriteLine($"Fetched {recordCount} time registrations");
record TokenResponse(string access_token, int expires_in, string token_type);For a production integration, use the cached token helper in Token Expiry and the paging helper in Pagination.
Next steps
Section titled “Next steps”- How Extraction Works — the pull pipeline at a glance
- Pagination — cursor-based paging and incremental sync
- Export Time Data — a complete export integration
- Projects & Work Orders — the full field reference