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.

GraphQL over HTTPS POST · OAuth2 Bearer auth · core operation assignJourneys

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 companyId and depotId, and confirm your API credentials.

You’ll do everything else from here using the API.

2. Endpoint & authentication

ItemValue
GraphQL endpointhttps://openapi.platform.tenix.eu/graphql
Auth endpointhttps://openapi.platform.tenix.eu/auth
Method (GraphQL)POST, Content-Type: application/json
Full schema referencetenix.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:

bashget-token.sh
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:

http
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.

Two shell tips

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.
  • assignJourneys is the one-step operation that places a vehicle on a block. This is what you’ll use for vehicle assignment.
Company Depot Block (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.

bashread — is the depot configured?
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" } }
  }'
Configured & ready

{"data":{"schedule":{"searchAssignedJourneys":[]}}} — the depot is configured. Proceed.

Not configured

“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:

graphql
mutation AssignJourneys($input: AssignJourneysInput!) {
  schedule {
    assignJourneys(input: $input)
  }
}

Variables:

jsonvariables
{
  "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:

bashassign.sh
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:

json
{ "data": { "schedule": { "assignJourneys": true } } }

Field reference

FieldMeaning
companyId / depotIdWhich depot to write to (from your Tenix contact)
blockIdThe vehicle-run identifier
dayThe operating day, as a full UTC timestamp (see §7)
journeys[]One or more trips
journeys[].vehicleNumberThe vehicle
journeys[].lineThe route/line
journeys[].distanceTrip distance in metres
journeys[].externalIdYour own reference for the journey (optional, but useful for verification and removal)
journeys[].startPoint / endPointWhere 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.

graphqlsearchAssignedJourneys
query SearchAssigned($f: SearchAssignedJourneysFilter!) {
  schedule {
    searchAssignedJourneys(filter: $f) {
      vehicleNumber blockId day line externalId type
      startPoint { name time }
      endPoint { name time }
    }
  }
}
jsonvariables
{ "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).

graphqlfindAssignedJourney
query Find($f: FindAssignedJourneyFilter!) {
  schedule {
    findAssignedJourney(filter: $f) {
      vehicleNumber blockId day externalId type
      startPoint { name time } endPoint { name time }
    }
  }
}
jsonvariables
{ "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).

Not-found behaviour

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

  1. day must be a full UTC timestamp (e.g. 2030-01-01T00:00:00Z), not a bare date, even though the schema types the field as Date. A bare date can fail to parse at the backend.
  2. For assignJourneys, day must be today or later. Past days are rejected with “Assignment day must not be earlier than today.”
  3. Local-timezone normalisation. Your depot is configured with a local timezone. The stored day may come back normalised to the depot’s local operating day, so a day sent 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 block day shifts — journey startPoint.time / endPoint.time are not affected; they read back exactly as sent. When you re-query or remove a block, key on the day you sent, not the shifted readback.
  4. Stop points on the assignment layer carry only name + time. There is no field for external stop IDs (e.g. NSR/Quay) on assignJourneys. Stop-point identifiers live only on the schedule-only layer (§9).
  5. The searchAssignedJourneys filter needs a blockId or vehicleNumber. companyId + depotId + day alone will return an error.
  6. Times are UTC. Send startPoint.time / endPoint.time as full UTC timestamps.

Timezone readback (verified live, both offsets)

Sent (day)Read backDepot offset
2030-01-01T00:00:00Z2029-12-31T23:00:00ZUTC+1 (CET, winter)
2026-08-20T00:00:00Z2026-08-19T22:00:00ZUTC+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:

SERVICE
The vehicle is in service on a route.
DEAD_RUN
The vehicle is driving between the depot and the start or end of a route (a deadhead run, no passengers), in either direction.
DEPOT_PARKING
For parking allocation. Not yet implemented.

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 a vehicleNumber; registers the journey and its vehicle assignment together. Returns Boolean.
  • 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 (via assignVehicleJourneys).
Important caution

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:

  • assignJourneys with two journeys sharing one externalIdtrue, both land (searchAssignedJourneys returns both).
  • findAssignedJourney on that shared externalId → 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. externalId is your value, so compose it to be unique per journey — e.g. block + trip index, not just line / vehicle.
  • When you must see every journey on a block, use searchAssignedJourneys by blockId. A duplicate externalId will 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.

graphql
mutation Remove($input: RemoveAssignedJourneyInput!) {
  schedule {
    removeAssignedJourney(input: $input)
  }
}
jsonvariables
{ "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:

bashremove.sh
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.

MessageMeaning / fix
Depot … is not configuredThe depot isn’t set up for scheduling. Contact Tenix.
Assignment day must not be earlier than todayUse a day of today or later.
BlockId or VehicleNumber must be providedAdd 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 / UNAUTHENTICATEDToken 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 implementedThe 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

  1. Get a token (§2).
  2. Confirm the depot is configured, so the read returns [], not “not configured” (§4).
  3. Assign a vehicle with assignJourneys, and expect true (§5).
  4. Verify with searchAssignedJourneys, or findAssignedJourney by externalId, and your vehicle comes back (§6).
  5. Remove test data with removeAssignedJourney when 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) and findAssignedJourney (single, by externalId) return the journey.
  • Remove (§10): matches on day + vehicleNumber + start/end times, returns true.
  • Journey type (§8): assigned journeys return a type (SERVICE / DEAD_RUN / DEPOT_PARKING); observed SERVICE in testing.
  • Timezone normalisation (§7): DST-aware, day only, point times unaffected.
  • null on not-found; no uniqueness enforcement on externalId (§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