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/v1Authentication
Every request needs a tenant API key sent as a Bearer token.
Authorization: Bearer st_live_your_key_hereCreate 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/calculate | Calculate toll cost for one trip. |
| POST | /api/v1/trips/bulk | Calculate a batch of trips in one call. |
| GET | /api/v1/trips | List previously calculated trips. |
| GET | /api/v1/trips/{id} | Retrieve one trip with its full breakdown. |
| GET | /api/v1/usage | Read the company's current shared calculation allowance and service state.read |
| GET | /api/v1/vehicles | List vehicle profiles. |
| POST | /api/v1/vehicles | Create 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-points | Browse passage points by country and type. |
| GET | /api/v1/rates | Browse published rate rules. |
| GET | /api/v1/ferry-fares | Search published ferry connectionsread |
| POST | /api/v1/ferry-fares/quote | Look up a selected-connection ferry tariffread |
| GET | /api/v1/api-keys | List API keys.admin |
| POST | /api/v1/api-keys | Create an API key.admin |
| DELETE | /api/v1/api-keys/{id} | Revoke an API key.admin |
| GET | /api/v1/health | Operational health check. |
| GET | /api/v1/ready | Container/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.
| Plan | Per hour | Per day | Bulk size |
|---|---|---|---|
| free | 100 | 1,000 | 10 |
| standard | 1,000 | 25,000 | 100 |
| premium | 5,000 | 100,000 | 500 |
| enterprise | 10,000 | 250,000 | 1000 |
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.