Vehicle Assignment API
Push vehicle-on-block assignments — “vehicle runs” — into a Tenix depot over the Schedule API, then verify the result. Written for integrators connecting a route-planning or scheduling system to Tenix.
Throughout, replace the placeholders <YOUR_COMPANY_ID> and <YOUR_DEPOT_ID> with the values your Tenix contact provides.
1. Before you start
Your Tenix contact will:
- Create and configure a test depot for you. Configuration — setting the depot’s timezone — is a Tenix-side step. A depot that merely exists but isn’t configured will reject schedule calls. See §4.
- Give you your
companyIdanddepotId, and confirm your API credentials.
You’ll do everything else from here using the API.
2. Endpoint & authentication
| Item | Value |
|---|---|
| GraphQL endpoint | https://openapi.platform.tenix.eu/graphql |
| Auth endpoint | https://openapi.platform.tenix.eu/auth |
| Method (GraphQL) | POST, Content-Type: application/json |
| Full schema reference | tenix.tech/graphql |
Authentication is OAuth2 password grant: exchange your username and password for a token, then send that token as a Bearer header on every call.
Get a token:
curl -s -X POST "https://openapi.platform.tenix.eu/auth" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=YOUR_USER&password=YOUR_PASS&scope=openapi&grant_type=password"
The response contains an access_token. Send it on every GraphQL call:
Authorization: Bearer <access_token>
Tokens expire (see expires_in in the auth response). When a call returns UNAUTHENTICATED / “jwt expired”, simply re-authenticate and use the new token.
Keep your credentials in environment variables rather than typing them into commands, so they don’t end up in your shell history.
In bash, single-quote the entire -d '...' GraphQL payload — the ! in type names like AssignJourneysInput! triggers history expansion (“event not found”) if left unquoted.
3. The model in brief
- A block is a vehicle run, identified by
companyId+depotId+blockId+day. - A journey is a single trip (start point/time → end point/time), optionally carrying a
vehicleNumber. assignJourneysis the one-step operation that places a vehicle on a block. This is what you’ll use for vehicle assignment.
(blockId)▶
Journeys
Each assigned journey is a vehicle placed on the block.
There is also a separate schedule-only layer, registerScheduledJourneys, which registers a timetable without a vehicle. Most integrations that assign vehicles should use assignJourneys. See §9.
4. Step 1 — Confirm your depot is ready
Before assigning anything, confirm the depot is configured. Run a read: a configured depot returns a clean (possibly empty) list, an unconfigured one returns an explicit error.
curl -s -X POST "https://openapi.platform.tenix.eu/graphql" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"query": "query($f: SearchAssignedJourneysFilter!){ schedule { searchAssignedJourneys(filter:$f){ blockId } } }",
"variables": { "f": { "companyId":"<YOUR_COMPANY_ID>", "depotId":"<YOUR_DEPOT_ID>", "day":"2030-01-01T00:00:00Z", "blockId":"BLOCK-001" } }
}'
{"data":{"schedule":{"searchAssignedJourneys":[]}}} — the depot is configured. Proceed.
“Depot … is not configured” — the depot hasn’t been set up for scheduling yet. Contact Tenix before continuing.
5. Step 2 — Assign a vehicle
The core operation. This places a vehicle on a block in a single call.
Mutation:
mutation AssignJourneys($input: AssignJourneysInput!) {
schedule {
assignJourneys(input: $input)
}
}
Variables:
{
"input": {
"companyId": "<YOUR_COMPANY_ID>",
"depotId": "<YOUR_DEPOT_ID>",
"blockId": "BLOCK-001",
"day": "2030-01-01T00:00:00Z",
"journeys": [
{
"vehicleNumber": "101",
"line": "1",
"distance": 12000,
"externalId": "test-001",
"startPoint": { "name": "Central Station", "time": "2030-01-01T06:00:00Z" },
"endPoint": { "name": "Harbour Terminal", "time": "2030-01-01T06:40:00Z" }
}
]
}
}
As a single curl call:
curl -s -X POST "https://openapi.platform.tenix.eu/graphql" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"query": "mutation($input: AssignJourneysInput!){ schedule { assignJourneys(input:$input) } }",
"variables": { "input": { "companyId":"<YOUR_COMPANY_ID>", "depotId":"<YOUR_DEPOT_ID>", "blockId":"BLOCK-001", "day":"2030-01-01T00:00:00Z", "journeys":[ { "vehicleNumber":"101", "line":"1", "distance":12000, "externalId":"test-001", "startPoint":{"name":"Central Station","time":"2030-01-01T06:00:00Z"}, "endPoint":{"name":"Harbour Terminal","time":"2030-01-01T06:40:00Z"} } ] } }
}'
A successful call returns:
{ "data": { "schedule": { "assignJourneys": true } } }
Field reference
| Field | Meaning |
|---|---|
companyId / depotId | Which depot to write to (from your Tenix contact) |
blockId | The vehicle-run identifier |
day | The operating day, as a full UTC timestamp (see §7) |
journeys[] | One or more trips |
journeys[].vehicleNumber | The vehicle |
journeys[].line | The route/line |
journeys[].distance | Trip distance in metres |
journeys[].externalId | Your own reference for the journey (optional, but useful for verification and removal) |
journeys[].startPoint / endPoint | Where and when: name + time only (see §7) |
6. Step 3 — Verify it landed
assignJourneys returns only true, so confirm the result with a read. The filter must include a blockId or vehicleNumber.
query SearchAssigned($f: SearchAssignedJourneysFilter!) {
schedule {
searchAssignedJourneys(filter: $f) {
vehicleNumber blockId day line externalId type
startPoint { name time }
endPoint { name time }
}
}
}
{ "f": {
"companyId": "<YOUR_COMPANY_ID>",
"depotId": "<YOUR_DEPOT_ID>",
"day": "2030-01-01T00:00:00Z",
"blockId": "BLOCK-001"
} }
Your assigned vehicle should come back in the list. (Note the day may read back shifted to the depot’s local operating day; see §7.)
6a. Verifying a single journey by your own externalId
When you only need to confirm one specific journey, findAssignedJourney is a tighter round-trip: it looks the journey up by the externalId you set on it and returns a single AssignedJourney (or null).
The filter requires all four fields: companyId, depotId, day, externalId. As with searchAssignedJourneys, the day you pass is the day you sent on assignment, not the normalised value it reads back (see §7).
query Find($f: FindAssignedJourneyFilter!) {
schedule {
findAssignedJourney(filter: $f) {
vehicleNumber blockId day externalId type
startPoint { name time } endPoint { name time }
}
}
}
{ "f": {
"companyId": "<YOUR_COMPANY_ID>",
"depotId": "<YOUR_DEPOT_ID>",
"day": "2030-01-01T00:00:00Z",
"externalId": "test-001"
} }
This makes externalId a usable idempotency/verification key end to end: set it on every journey at assignment time, then confirm each one with a single find rather than paging a list. Setting externalId is optional in the schema, but do it — it is what enables this path (and precise removal).
An externalId that matches nothing returns findAssignedJourney: null cleanly, with no GraphQL error. A client can treat null as “not found” directly, without wrapping the call in error handling. (Verified live.)
7. Rules & gotchas
daymust be a full UTC timestamp (e.g.2030-01-01T00:00:00Z), not a bare date, even though the schema types the field asDate. A bare date can fail to parse at the backend.- For
assignJourneys,daymust be today or later. Past days are rejected with “Assignment day must not be earlier than today.” - Local-timezone normalisation. Your depot is configured with a local timezone. The stored
daymay come back normalised to the depot’s local operating day, so adaysent as midnight UTC can read back offset by the local timezone. This is expected, and it is DST-aware: the offset tracks the depot’s actual offset on that date. Only the blockdayshifts — journeystartPoint.time/endPoint.timeare not affected; they read back exactly as sent. When you re-query or remove a block, key on thedayyou sent, not the shifted readback. - Stop points on the assignment layer carry only
name+time. There is no field for external stop IDs (e.g. NSR/Quay) onassignJourneys. Stop-point identifiers live only on the schedule-only layer (§9). - The
searchAssignedJourneysfilter needs ablockIdorvehicleNumber.companyId+depotId+dayalone will return an error. - Times are UTC. Send
startPoint.time/endPoint.timeas full UTC timestamps.
Timezone readback (verified live, both offsets)
Sent (day) | Read back | Depot offset |
|---|---|---|
| 2030-01-01T00:00:00Z | 2029-12-31T23:00:00Z | UTC+1 (CET, winter) |
| 2026-08-20T00:00:00Z | 2026-08-19T22:00:00Z | UTC+2 (CEST, summer) |
8. Journey type
Every assigned journey carries a type, returned as AssignedJourney.type. The schema defines three values via the JourneyType enum:
The type is set by the platform, not supplied on assignment (JourneyInput has no type field). In testing, assigned journeys came back as SERVICE.
9. Two layers: assignment vs. schedule-only
There are two ways to push journeys, for different purposes:
assignJourneys(recommended for vehicle assignment). One-step, and carries avehicleNumber; registers the journey and its vehicle assignment together. ReturnsBoolean.registerScheduledJourneys: schedule-only. Registers a planned timetable with no vehicle. Its start/end points use a richer input that does support stop IDs and coordinates. Returns a list of registered journey IDs. Vehicles are attached separately (viaassignVehicleJourneys).
On a depot that is not configured, registerScheduledJourneys returns an empty list [] rather than an error — so it looks like a success but nothing is stored. If you use this operation and get an empty list back, verify the depot is configured (§4) before assuming your payload is wrong.
assignJourneys returns a nullable Boolean
The field is typed Boolean (nullable), not Boolean!, so it can in principle return null as well as false. A robust client should treat anything other than true as “not confirmed” and follow up with a read (§6), rather than checking for !== false. Never treat the boolean as sole proof the write landed; the read is the source of truth.
9a. externalId uniqueness is not enforced
The platform does not enforce uniqueness of externalId. Two journeys in the same block can carry the same externalId; both are accepted and both are stored. Verified live:
assignJourneyswith two journeys sharing oneexternalId→true, both land (searchAssignedJourneysreturns both).findAssignedJourneyon that sharedexternalId→ returns only one of them (the earlier journey in the block), silently. No error, no warning.
Implication for integrations:
- Treat
findAssignedJourney(externalId)as “find a match,” not “find the match.” It is safe as a verification/idempotency key only if uniqueness is guaranteed upstream. - Guarantee uniqueness in the source system.
externalIdis your value, so compose it to be unique per journey — e.g.block + trip index, not justline/vehicle. - When you must see every journey on a block, use
searchAssignedJourneysbyblockId. A duplicateexternalIdwill hide journeys from find but never from search.
10. Removing test data
To clean up a test assignment, use removeAssignedJourney. The match fields must line up exactly with what you assigned.
mutation Remove($input: RemoveAssignedJourneyInput!) {
schedule {
removeAssignedJourney(input: $input)
}
}
{ "input": {
"companyId": "<YOUR_COMPANY_ID>",
"depotId": "<YOUR_DEPOT_ID>",
"blockId": "BLOCK-001",
"day": "2030-01-01T00:00:00Z",
"vehicleNumber": "101",
"startPointTime": "2030-01-01T06:00:00Z",
"endPointTime": "2030-01-01T06:40:00Z"
} }
As a single curl call:
curl -s -X POST "https://openapi.platform.tenix.eu/graphql" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"query": "mutation($input: RemoveAssignedJourneyInput!){ schedule { removeAssignedJourney(input:$input) } }",
"variables": { "input": { "companyId":"<YOUR_COMPANY_ID>", "depotId":"<YOUR_DEPOT_ID>", "blockId":"BLOCK-001", "day":"2030-01-01T00:00:00Z", "vehicleNumber":"101", "startPointTime":"2030-01-01T06:00:00Z", "endPointTime":"2030-01-01T06:40:00Z" } }
}'
removeAssignedJourney matches on start/end times, so remove one journey per call. Re-run the §6 read afterwards; an empty list confirms it’s gone.
11. Troubleshooting
Error messages and what they mean.
| Message | Meaning / fix |
|---|---|
Depot … is not configured | The depot isn’t set up for scheduling. Contact Tenix. |
Assignment day must not be earlier than today | Use a day of today or later. |
BlockId or VehicleNumber must be provided | Add a blockId or vehicleNumber to your filter. |
Failed to read request (HTTP 400) | Usually a day sent as a bare date instead of a full UTC timestamp. |
jwt expired / UNAUTHENTICATED | Token expired; re-authenticate (§2). |
registerScheduledJourneys returns [] | Nothing was registered. On an unconfigured depot this is silent, so confirm the depot is configured (§4). |
Not yet implemented | The operation exists in the schema but isn’t available yet. Don’t rely on it. |
event not found (in bash) | The ! in a type name triggered shell history expansion. Single-quote the whole -d '...' payload. |
12. End-to-end checklist
- Get a token (§2).
- Confirm the depot is configured, so the read returns
[], not “not configured” (§4). - Assign a vehicle with
assignJourneys, and expecttrue(§5). - Verify with
searchAssignedJourneys, orfindAssignedJourneybyexternalId, and your vehicle comes back (§6). - Remove test data with
removeAssignedJourneywhen done (§10).
Once you can complete steps 3–4, your integration is proven end-to-end; everything after that is mapping your own scheduling output onto these fields.
Verified behaviour summary
Everything the guide describes has been exercised end to end against the live API. Confirmed:
- Config gate (§4): unconfigured depot rejects the read; configured returns
[]. - Assign (§5): single call,
true, vehicle on block. - Verify (§6): both
searchAssignedJourneys(list) andfindAssignedJourney(single, byexternalId) return the journey. - Remove (§10): matches on
day+vehicleNumber+ start/end times, returnstrue. - Journey type (§8): assigned journeys return a
type(SERVICE/DEAD_RUN/DEPOT_PARKING); observedSERVICEin testing. - Timezone normalisation (§7): DST-aware,
dayonly, point times unaffected. nullon not-found; no uniqueness enforcement onexternalId(§9a).
Schema reference: tenix.tech/graphql
Depot configuration, credentials & integration questions: your Tenix contact or support@tenix.eu
Saga Tenix AS · www.tenix.eu · +47 47 77 00 70