Machine Integration
This guide covers how to send excavator operating states (idle, moving, working) from an external system into Ditio. It is the integration point for telematics providers and fleet management systems.
When to use this
Section titled “When to use this”- You have a fleet management system that tracks excavator operating states
- You want machine utilization data to appear in Ditio
- You need to sync historical machine data (daily batch uploads)
- You’re a telematics provider integrating with Ditio on behalf of a customer
Prerequisites
Section titled “Prerequisites”- API credentials with the
masstransportapiv1scope — see Authentication - The Ditio company ID of the company your API client belongs to. Ask your
Ditio contact for it during onboarding. The examples read it from
DITIO_COMPANY_ID - No Ditio project or machine IDs: you identify machines and projects with
your own IDs (
externalMachineId,externalProjectId)
Examples use the environment variables from
Environments (the Mass
Transport API is $DITIO_MASSTRANSPORT_BASE) and default to the test
environment. Get a token with scope=masstransportapiv1 into $TOKEN (set
DITIO_SCOPE=masstransportapiv1), as shown in
Getting a token in your scripts.
How it works
Section titled “How it works”Each record is one sample at one point in time: machine X, on project Y, was in state S at time T, optionally with its position. Ditio works out how long a machine spent in each state from consecutive samples. You don’t send start and end times.
| Field | Type | Description |
|---|---|---|
companyId | string | Your Ditio company ID. Send it on every record; the checkpoint and the aggregation find records by it |
externalMachineId | string | Your ID for the machine |
externalProjectId | string | Your ID for the project or site |
timeStamp | string (date-time) | When the sample was taken. Send UTC with a Z suffix |
state | integer | 0 = Idle, 1 = Moving, 2 = Working. Send the number: a string such as "Working" is rejected with 400 |
geoPoint | object | Optional position as GeoJSON: {"type": "Point", "coordinates": [longitude, latitude]} |
The full schema, every endpoint and its responses are in the External Excavator State reference.
The typical flow:
- Read the checkpoint: the time of the newest sample Ditio has for a project
- Collect the samples newer than that from your telematics data
- Send them to Ditio in one
importrequest - Repeat on a schedule
Step-by-step
Section titled “Step-by-step”1. Read the sync checkpoint
Section titled “1. Read the sync checkpoint”curl -H "Authorization: Bearer $TOKEN" \ "$DITIO_MASSTRANSPORT_BASE/api/external-excavator-state/latest-timestamp/$DITIO_COMPANY_ID/SITE-0042"The path takes your Ditio company ID and your project ID (the
externalProjectId you send). The response body is a bare number: the
timestamp of the newest sample, in Unix seconds. It is 0 when Ditio has no
samples for that company and project yet.
2. Import samples in bulk
Section titled “2. Import samples in bulk”Send the samples newer than the checkpoint in one request:
curl -X POST "$DITIO_MASSTRANSPORT_BASE/api/external-excavator-state/import" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '[ { "companyId": "'"$DITIO_COMPANY_ID"'", "externalMachineId": "EX-001", "externalProjectId": "SITE-0042", "timeStamp": "2026-01-15T08:00:00Z", "state": 2, "geoPoint": { "type": "Point", "coordinates": [10.7522, 59.9139] } }, { "companyId": "'"$DITIO_COMPANY_ID"'", "externalMachineId": "EX-001", "externalProjectId": "SITE-0042", "timeStamp": "2026-01-15T08:45:00Z", "state": 0 } ]'import returns 200 OK with the records it stored. It is safe to resend: a
sample with the same company, machine, project and timeStamp as an existing
one updates that sample’s state and geoPoint instead of adding a second
one.
To send a single sample, POST one object to
/api/external-excavator-state. It returns 201 Created.
3. Query aggregated data
Section titled “3. Query aggregated data”To check what Ditio has, ask for the totals per state for one project:
curl -X POST "$DITIO_MASSTRANSPORT_BASE/api/external-excavator-state/aggregated-data-per-state" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "projectId": "SITE-0042", "dateFrom": "2026-01-01", "dateTo": "2026-01-31" }'projectIdis yourexternalProjectId. Send it: only samples whose project matches it are counted.dateToincludes that whole day.- The optional
excavatorIdslist is matched against theidDitio gave each stored sample, not against yourexternalMachineId. Leave it out to include every machine. - Only samples stored under your company are counted.
The response has one entry per state, keyed by the state number as a string
("0", "1", "2").
Complete example (Python)
Section titled “Complete example (Python)”import osfrom datetime import datetime, timezone
import requests# Paste get_token (/getting-started/authentication/#getting-a-token-in-your-scripts) above this.
MASSTRANSPORT_BASE = os.environ.get("DITIO_MASSTRANSPORT_BASE", "https://core-api.ditio.dev/masstransport")BASE_URL = f"{MASSTRANSPORT_BASE}/api/external-excavator-state"COMPANY_ID = os.environ["DITIO_COMPANY_ID"]PROJECT_ID = "SITE-0042" # your own project ID
IDLE, MOVING, WORKING = 0, 1, 2
headers = {"Authorization": f"Bearer {get_token('masstransportapiv1')}"}
# Step 1: the checkpoint is a bare number (Unix seconds); 0 means no data yet.response = requests.get( f"{BASE_URL}/latest-timestamp/{COMPANY_ID}/{PROJECT_ID}", headers=headers, timeout=30,)response.raise_for_status()checkpoint = datetime.fromtimestamp(int(response.json()), tz=timezone.utc)print(f"Newest sample in Ditio: {checkpoint.isoformat()}")
# Step 2: collect newer samples from your telematics data (your logic).samples = [ ("EX-001", datetime(2026, 1, 15, 7, 0, tzinfo=timezone.utc), WORKING), ("EX-001", datetime(2026, 1, 15, 9, 30, tzinfo=timezone.utc), IDLE),]new_records = [ { "companyId": COMPANY_ID, "externalMachineId": machine_id, "externalProjectId": PROJECT_ID, "timeStamp": taken_at.strftime("%Y-%m-%dT%H:%M:%SZ"), "state": state, } for machine_id, taken_at, state in samples if taken_at > checkpoint]
# Step 3: bulk import. Resending a sample updates it rather than duplicating it.if new_records: response = requests.post(f"{BASE_URL}/import", headers=headers, json=new_records, timeout=60) response.raise_for_status() # 200 OK on success print(f"Imported {len(new_records)} samples")Complete example (C#)
Section titled “Complete example (C#)”using System.Net.Http.Headers;using System.Net.Http.Json;
var massTransportBase = Environment.GetEnvironmentVariable("DITIO_MASSTRANSPORT_BASE") ?? "https://core-api.ditio.dev/masstransport";var baseUrl = $"{massTransportBase}/api/external-excavator-state";var companyId = Environment.GetEnvironmentVariable("DITIO_COMPANY_ID")!;const string projectId = "SITE-0042"; // your own project ID
using var http = new HttpClient();// DitioAuth.GetTokenAsync: see /getting-started/authentication/#token-expiryhttp.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", await DitioAuth.GetTokenAsync(http, "masstransportapiv1"));
// Step 1: the checkpoint is a bare number (Unix seconds); 0 means no data yet.var latestSeconds = await http.GetFromJsonAsync<long>( $"{baseUrl}/latest-timestamp/{Uri.EscapeDataString(companyId)}/{Uri.EscapeDataString(projectId)}");var checkpoint = DateTimeOffset.FromUnixTimeSeconds(latestSeconds);
// Step 2: collect newer samples from your telematics data (your logic).var newRecords = new[] { new ExcavatorStateSample(companyId, "EX-001", projectId, new DateTime(2026, 1, 15, 7, 0, 0, DateTimeKind.Utc), MachineState.Working), new ExcavatorStateSample(companyId, "EX-001", projectId, new DateTime(2026, 1, 15, 9, 30, 0, DateTimeKind.Utc), MachineState.Idle), } .Where(sample => sample.TimeStamp > checkpoint.UtcDateTime) .ToList();
// Step 3: bulk import. PostAsJsonAsync writes camelCase names and the state as a number.if (newRecords.Count > 0){ var response = await http.PostAsJsonAsync($"{baseUrl}/import", newRecords); response.EnsureSuccessStatusCode(); // 200 OK on success Console.WriteLine($"Imported {newRecords.Count} samples");}
enum MachineState { Idle = 0, Moving = 1, Working = 2 }
record ExcavatorStateSample( string CompanyId, string ExternalMachineId, string ExternalProjectId, DateTime TimeStamp, MachineState State);Common issues
Section titled “Common issues”| Error | Cause | Fix |
|---|---|---|
400 with ArgumentIsNull | Empty array or no body sent to import | Send at least one record |
400 validation error on the body | A field has the wrong type, typically state sent as a string | Send state as 0, 1 or 2, and timeStamp as an ISO 8601 date-time |
401 Unauthorized | Token expired, or issued for another scope — this API needs masstransportapiv1 | Refresh once per Token Expiry; if it persists, see 401 — wrong scope |
Checkpoint stays 0 after an import | The companyId or project in the checkpoint path doesn’t match what you sent | Use your Ditio company ID in every record and the same externalProjectId in the path |
| Aggregation returns zeros | projectId missing or not your externalProjectId, excavatorIds set to your own machine IDs, or records imported with another companyId | See Query aggregated data |
Related
Section titled “Related”- External Excavator State — schema and endpoint reference
- Weighbridge Integration — push weighbridge data to Ditio
- Machine Data — sync the machine catalogue and export machine usage