Back to home
REST API · v1

SagaToll API

Calculate toll, bridge and ferry costs for commercial routes straight from your own systems. JSON in, an explainable cost breakdown out.

Coverage today: Norway, partially (most toll stations and about half of the ferry connections), and a small number of Swedish bridges. Denmark and Finland are accepted as countries but have no priced crossings, so such routes come back incomplete with no total. Check pricingStatus and warnings on every result.

Base URL

All endpoints are served under the versioned API path.

https://sagatoll.sagabuild.com/api/v1

Authentication

Every request needs a tenant API key sent as a Bearer token.

Authorization: Bearer st_live_your_key_here

Create and revoke keys under Dashboard → API & access. Keys are shown in plaintext only once at creation; SagaToll stores a salted hash and a lookup digest. The st_test_ prefix for test label and st_live_ live label do not create separate databases. Both act on the authenticated tenant; use a separate tenant/environment for destructive testing. Keep keys on your backend, not in public frontend code.

Quick start

Calculate the toll cost of a single truck trip.

curl -X POST https://sagatoll.sagabuild.com/api/v1/trips/calculate \
  -H "Authorization: Bearer st_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "origin": { "latitude": 59.2058, "longitude": 10.9552, "address": "Fredrikstad" },
    "destination": { "latitude": 59.9132, "longitude": 10.6379, "address": "Bærum" },
    "departureAt": "2026-04-13T06:45:00+02:00",
    "vehicle": {
      "type": "truck",
      "weightKg": 18000,
      "axleCount": 3,
      "euroClass": "euro6",
      "fuelType": "diesel",
      "hasAutopass": true,
      "lengthCm": 1200
    },
    "options": { "currency": "NOK", "includeRoute": true }
  }'

Amounts are returned in minor units (øre/cent) plus a localized formatted string. A per-line breakdown explains which rule priced each passage.

Success is HTTP 201. Always check pricingStatus: complete, estimated or incomplete. Incomplete quotes return total.amount: null; unmatched line amounts are null too. pricedSubtotal covers priced passages only, not the entire journey. Never convert null to zero.

Price your own route

Send your TMS geometry to see which crossings it passes, without re-routing. Arrival times remain unknown, so nothing is priced.

// inside the calculate request body
"route": {
  "coordinates": [
    { "latitude": 59.2058, "longitude": 10.9552 },
    { "latitude": 59.7400, "longitude": 10.8000 },
    { "latitude": 59.9132, "longitude": 10.6379 }
  ]
}

Add an optional route object. When present, SagaToll skips its own routing and matches your geometry directly. Send the full path (not just the stops) so passage matching is accurate. Without it, SagaToll routes through up to 25 waypoints itself using HERE. Geometry alone does not provide arrival times, so no passage is priced: every line amount and the total are null, and the call still counts as one calculation. A supplied route may have at most 10,000 points and describe at most 5,000 km. HERE passage timing is estimated and labelled accordingly.

Published ferry fares

Requires read permission, including for this POST. Explicit Norway travel date, vehicle and payment agreement are required. Amounts are integer NOK ore. A known connection with missing, unsupported, ambiguous, expired or stale tariffs returns 200 with status unavailable and a null amount. This is not a sailing, timetable, capacity, booking or invoice check. Private, no-store; no trip is saved and no calculation quota is consumed.

Search /ferry-fares?q=Flora&limit=20&offset=0, then send the returned connection id as connectionId to /ferry-fares/quote. Include a real travelDate in YYYY-MM-DD format, a vehicle with type, length in centimetres, weight in kilograms and fuel type, plus the payment agreement. The OpenAPI request schema lists the supported values.

Endpoints

Reference data and stored trips need read; trip calculations and vehicle writes need write; key management needs admin. Ferry tariff lookups, including POST, need read. Health probes are public.

POST/api/v1/trips/calculateCalculate toll cost for one trip.
POST/api/v1/trips/bulkCalculate a batch of trips in one call.
GET/api/v1/tripsList previously calculated trips.
GET/api/v1/trips/{id}Retrieve one trip with its full breakdown.
GET/api/v1/usageRead the company's current shared calculation allowance and service state.read
GET/api/v1/vehiclesList vehicle profiles.
POST/api/v1/vehiclesCreate a vehicle profile.
GET/api/v1/vehicles/{id}Retrieve one vehicle profile.
PUT/api/v1/vehicles/{id}Update a vehicle profile.
DELETE/api/v1/vehicles/{id}Delete a vehicle profile.
GET/api/v1/toll-pointsBrowse passage points by country and type.
GET/api/v1/ratesBrowse published rate rules.
GET/api/v1/ferry-faresSearch published ferry connectionsread
POST/api/v1/ferry-fares/quoteLook up a selected-connection ferry tariffread
GET/api/v1/api-keysList API keys.admin
POST/api/v1/api-keysCreate an API key.admin
DELETE/api/v1/api-keys/{id}Revoke an API key.admin
GET/api/v1/healthOperational health check.
GET/api/v1/readyContainer/database readiness check.

Responses & errors

Every response uses a consistent envelope.

Success

{
  "data": { /* ... */ },
  "meta": { "requestId": "..." }
}

Error

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "...",
    "details": null
  },
  "meta": { "requestId": "..." }
}

Common codes: UNAUTHORIZED (401), FORBIDDEN (403), INVALID_TRIP_PAYLOAD (400), RATE_LIMIT_EXCEEDED (429), TENANT_USAGE_LIMIT_EXCEEDED (429), SERVICE_INACTIVE (403), VEHICLE_NOT_FOUND (404), API_KEY_NAME_CONFLICT (409).

A 422 means the request is valid but cannot be calculated as sent, so change it before retrying: ROUTE_NOT_FOUND, ROUTE_NOT_USABLE (critical routing restrictions; no quote is stored), ROUTING_REQUEST_REJECTED, ROUTE_GEOMETRY_TOO_LARGE, UNSUPPORTED_CURRENCY_CONVERSION, BULK_SIZE_EXCEEDED. A 503 is temporary and worth retrying later: ROUTING_UNAVAILABLE, ROUTING_TIMEOUT, ROUTING_PROVIDER_ERROR, EXCHANGE_RATES_UNAVAILABLE. A 500 carries no detail; quote meta.requestId to support.

Rate limits

A tenant plan sets hourly and daily request ceilings that all of the tenant's API keys share; each key can also carry a lower hourly cap of its own. A separate company calculation allowance is shared by all keys and dashboard users. These are access controls, not subscription prices.

PlanPer hourPer dayBulk size
free1001,00010
standard1,00025,000100
premium5,000100,000500
enterprise10,000250,0001000

Read remaining quota from X-RateLimit-Remaining-Hour and X-RateLimit-Remaining-Day. On an API rate-limit 429, wait the number of seconds in Retry-After. Bulk responses that refund unstarted items omit quota headers rather than report outdated admission counters.

For monthly capacity, use GET /usage. Each persisted calculation, including an incomplete quote, consumes one unit. Bulk items count individually; a batch may partially succeed. Known failures release reservations. An interrupted job remains reserved until safely reconciled. Deleting a trip or replacing a key does not reset usage. A monthly quota error includes the UTC periodEnd; retrying immediately will not create capacity. Suspended or ended service blocks new calculations, not historical reads.

Cross-origin (CORS)

CORS support does not make a long-lived API key safe to publish in frontend code. Use your authenticated backend for customer-facing integrations.

All /api/v1 endpoints send permissive CORS headers and answer preflight OPTIONS requests, so your frontend can call SagaToll with an API key. Treat live keys as secrets — for public browser apps, proxy requests through your own backend.

Next step

Ready to integrate?