Skip to content

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.

  • 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
  • API credentials with the masstransportapiv1 scope — 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.

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.

FieldTypeDescription
companyIdstringYour Ditio company ID. Send it on every record; the checkpoint and the aggregation find records by it
externalMachineIdstringYour ID for the machine
externalProjectIdstringYour ID for the project or site
timeStampstring (date-time)When the sample was taken. Send UTC with a Z suffix
stateinteger0 = Idle, 1 = Moving, 2 = Working. Send the number: a string such as "Working" is rejected with 400
geoPointobjectOptional 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:

  1. Read the checkpoint: the time of the newest sample Ditio has for a project
  2. Collect the samples newer than that from your telematics data
  3. Send them to Ditio in one import request
  4. Repeat on a schedule
Terminal window
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.

Send the samples newer than the checkpoint in one request:

Terminal window
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.

To check what Ditio has, ask for the totals per state for one project:

Terminal window
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"
}'
  • projectId is your externalProjectId. Send it: only samples whose project matches it are counted.
  • dateTo includes that whole day.
  • The optional excavatorIds list is matched against the id Ditio gave each stored sample, not against your externalMachineId. 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").

import os
from 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")
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-expiry
http.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);
ErrorCauseFix
400 with ArgumentIsNullEmpty array or no body sent to importSend at least one record
400 validation error on the bodyA field has the wrong type, typically state sent as a stringSend state as 0, 1 or 2, and timeStamp as an ISO 8601 date-time
401 UnauthorizedToken expired, or issued for another scope — this API needs masstransportapiv1Refresh once per Token Expiry; if it persists, see 401 — wrong scope
Checkpoint stays 0 after an importThe companyId or project in the checkpoint path doesn’t match what you sentUse your Ditio company ID in every record and the same externalProjectId in the path
Aggregation returns zerosprojectId missing or not your externalProjectId, excavatorIds set to your own machine IDs, or records imported with another companyIdSee Query aggregated data