DEVELOPER PLATFORM

Build on Canada's
mobility infrastructure

REST APIs for carrier integration, parcel tracking and live GPS telemetry — with sovereign, in-region data handling.

Quick start

A carrier admin issues you a key from Manage → API Keys. Verify it first — /api/v1/ping tells you the key works and which carrier it is scoped to.

curl https://transia1.com/api/v1/ping \
  -H "Authorization: Bearer $TRANSIA_KEY"
{
  "ok": true,
  "operator": { "id": 3, "name": "Maple Transit" },
  "time": "2026-09-04T23:15:08-04:00"
}

Authentication

Two schemes, for two different callers.

Vendor API key Server to server, for a carrier's own systems. Send it as Authorization: Bearer or X-API-Key. Every request is scoped to the carrier that owns the key.
Device-bound JWT For mobile apps and SPAs. Exchange credentials at /api/auth/token, then send the access token with X-Device-Id. Tokens are pinned to the device that requested them.

Keys are stored only as a SHA-256 hash — the secret is shown once, at issue or rotation, and cannot be recovered afterwards. Lost one? Rotate it. A key also stops working the moment its carrier stops being active, so suspending a carrier closes every integration it holds without anyone hunting through this list.

API reference

11 endpoints, read from this deployment's own route table — so this list is what is actually running, not what someone remembered to write down.

Vendor API key

Server to server. One key belongs to one carrier and can never read another carrier's data.

The cheapest way to verify a newly issued or rotated key before you deploy it.

Vendor API key Try it below
curl https://transia1.com/api/v1/ping \
  -H "Authorization: Bearer $TRANSIA_KEY"
Vendor API key Try it below
curl https://transia1.com/api/v1/routes \
  -H "Authorization: Bearer $TRANSIA_KEY"

Only departures still in the future come back, so polling this endpoint never walks backwards through history.

Vendor API key Paginated · 50 / page Try it below
Query
pageoptional · 1-based page number, 50 rows per page
curl https://transia1.com/api/v1/schedules \
  -H "Authorization: Bearer $TRANSIA_KEY"

Newest first. Only bookings on your own routes are visible — the scope comes from the key, not from a filter you pass.

Vendor API key Paginated · 50 / page Try it below
Query
pageoptional · 1-based page number, 50 rows per page
curl https://transia1.com/api/v1/bookings \
  -H "Authorization: Bearer $TRANSIA_KEY"
Vendor API key Paginated · 50 / page Try it below
Query
pageoptional · 1-based page number, 50 rows per page
curl https://transia1.com/api/v1/parcels \
  -H "Authorization: Bearer $TRANSIA_KEY"

Runs the parcel state machine, so an illegal transition is refused with 422 rather than silently written. The legal moves are the ones drawn in the lifecycle below.

Vendor API key
Body
statusrequired · one of the parcel statuses
locationoptional · free text, max 160 chars
noteoptional · free text, max 240 chars
curl -X PATCH https://transia1.com/api/v1/parcels/{parcel}/status \
  -H "Authorization: Bearer $TRANSIA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "status": "out_for_delivery",
    "location": "Ottawa Hub",
    "note": "On van 12"
}'
Vendor API key Paginated · 50 / page Try it below
Query
pageoptional · 1-based page number, 50 rows per page
curl https://transia1.com/api/v1/freight-orders \
  -H "Authorization: Bearer $TRANSIA_KEY"

Rows are validated individually: a bad row comes back reported by index and the rest of the batch still lands, so one malformed vehicle never costs you the whole push.

Vendor API key 600 / minute
Body
positions[].vehicle_refrequired · your own vehicle id, max 64 chars
positions[].latrequired · -90 to 90
positions[].lngrequired · -180 to 180
positions[].reported_atrequired · ISO 8601 timestamp
positions[].route_codeoptional · a route code you operate
positions[].headingoptional · 0-359
positions[].speed_kmhoptional · 0-600
positions[].occupancy_pctoptional · 0-100
positions[].statusoptional · in_transit_to | stopped_at | incoming_at
positions[].stop_refoptional · max 64 chars
curl -X POST https://transia1.com/api/v1/telemetry/positions \
  -H "Authorization: Bearer $TRANSIA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "positions": [
        {
            "vehicle_ref": "BUS-114",
            "route_code": "B502",
            "lat": 43.6452,
            "lng": -79.3806,
            "heading": 84,
            "speed_kmh": 47,
            "occupancy_pct": 62,
            "status": "in_transit_to",
            "reported_at": "2026-01-01T09:00:00Z"
        }
    ]
}'

Device-bound JWT

For mobile apps and SPAs. Access tokens are pinned to the device that asked for them.

Device-bound JWT
curl https://transia1.com/api/auth/me \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "X-Device-Id: $DEVICE_ID"

No authentication

Callable without credentials.

The device id is bound into the token, so presenting it from another device is rejected. Send otp as well when the account has two-factor enabled.

No authentication 10 / minute
Body
emailrequired · account email
passwordrequired
device_idrequired · stable id for this install, max 128 chars
otpoptional · needed when the account has 2FA on
curl -X POST https://transia1.com/api/auth/token \
  -H "Content-Type: application/json" \
  -d '{
    "email": "ops@carrier.ca",
    "password": "your-password",
    "device_id": "ios-9f3c1b7e"
}'

Refresh tokens are bound to the same device as the token they replace.

No authentication 20 / minute
Body
refresh_tokenrequired · the refresh token from /api/auth/token
device_idrequired · must match the device the pair was issued to, max 128 chars
curl -X POST https://transia1.com/api/auth/token/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "device_id": "ios-9f3c1b7e"
}'

Try it live

Call this deployment with your own key and see the real response. Read-only endpoints only.

The request goes straight from your browser to this deployment's API. Your key is never sent anywhere else, is not stored, and is cleared when you leave the page — but it is still a live credential, so use a staging key if you would rather not type a production one into a browser. Press Ctrl + Enter to send.

Errors & rate limits

Failures come back as JSON with the reason in error or message.

401Missing API key, or the key is invalid, revoked, expired — or its carrier is no longer active.
403The key is valid but not for this carrier's data.
404The record does not exist, or belongs to another carrier. Deliberately indistinguishable, so the API cannot be used to probe for other carriers' ids.
422Validation failed, or a state machine refused the transition. The body names the offending fields.
429Rate limit exceeded. Limits are per key, so one carrier can never starve another.

Rate limits are shown on each endpoint above where one applies. Telemetry is deliberately generous — 600 / minute — because a whole fleet reports together.

Webhooks

Register an endpoint under Operator → Developer and subscribe to the events you want. Deliveries are signed.

parcel.status_changedParcel status changed
parcel.deliveredParcel delivered
booking.createdBooking created
disruption.reportedService disruption reported
pingTest ping

Delivery & retries

Attempts5, then the delivery is parked for inspection.
Backoff10 s, 1 min, 5 min, 15 min between attempts.
Timeout5 seconds per attempt.
SuccessAny 2xx. Anything else — including a timeout — counts as a failure and is retried.
X-Transia-EventThe event name, so you can route before you parse.
X-Transia-Signaturesha256=<hmac_sha256(raw_body, endpoint_secret)>

Retries mean the same event can arrive twice — a receiver that hangs up after processing will still be retried. Treat id as an idempotency key and ignore one you have already seen.

Payload

{
    "id": "evt_xxxxxxxxxxxxxxxxxxxx",
    "event": "parcel.status_changed",
    "created": "2026-01-01T09:00:00+00:00",
    "data": {
        "tracking_number": "TRP-4471903",
        "from": "in_transit",
        "to": "out_for_delivery",
        "location": "Ottawa Hub"
    }
}

Verifying the signature

HMAC the raw request body — not a re-encoded copy of the parsed JSON, whose key order and spacing will not match — with your endpoint secret, and compare in constant time.

$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, $endpointSecret);

if (! hash_equals($expected, $_SERVER['HTTP_X_TRANSIA_SIGNATURE'] ?? '')) {
    http_response_code(403);
    exit;
}

$event = json_decode($raw, true);

Parcel lifecycle

The states PATCH /api/v1/parcels/{parcel}/status enforces, and the moves each one allows. Anything not drawn here is refused with 422 — so this is the graph to code against, rather than discovering it one rejected transition at a time in production.

Created
created
may move to picked_up cancelled
Picked Up
picked_up
In Transit
in_transit
Out for Delivery
out_for_delivery
may move to delivered exception
Delivered
delivered
Terminal — nothing follows it.
Exception
exception
Returned
returned
Terminal — nothing follows it.
Cancelled
cancelled
Terminal — nothing follows it.

GPS telemetry

Push real vehicle positions and the live map stops being a simulation. Batch up to 1000 rows a call; each row is validated on its own, so one bad vehicle never costs you the batch.

curl -X POST https://transia1.com/api/v1/telemetry/positions \
  -H "Authorization: Bearer $TRANSIA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "positions": [
      {
        "vehicle_ref": "BUS-114",
        "route_code": "B502",
        "lat": 43.6452, "lng": -79.3806,
        "heading": 84, "speed_kmh": 47,
        "occupancy_pct": 62,
        "status": "in_transit_to",
        "reported_at": "2026-09-04T23:15:08-04:00"
      }
    ]
  }'

Parcel status pushes accept 8 states — the lifecycle above shows which of them each state may move to. Illegal transitions are refused with 422 rather than written.