{"openapi":"3.1.0","info":{"title":"SagaToll API","version":"1.0.0","description":"Calculate Nordic toll, bridge, ferry, and congestion-zone costs for commercial routes. Authenticate every request with a tenant API key sent as a Bearer token.","contact":{"name":"SagaToll","url":"https://0.0.0.0:3000"}},"servers":[{"url":"https://0.0.0.0:3000/api/v1","description":"SagaToll API"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Trips","description":"Calculate and retrieve toll-cost calculations."},{"name":"Vehicles","description":"Manage reusable vehicle profiles."},{"name":"Reference","description":"Browse toll points and published rates."},{"name":"Access","description":"Manage API keys for the tenant."},{"name":"System","description":"Service health."}],"paths":{"/health":{"get":{"tags":["System"],"summary":"Operational health check","security":[],"responses":{"200":{"description":"Service is operationally healthy."},"503":{"description":"Service is degraded or unavailable."}}}},"/ready":{"get":{"tags":["System"],"summary":"Container readiness check","description":"Checks the runtime can reach the database. The release Docker healthcheck uses this endpoint.","security":[],"responses":{"200":{"description":"Service is ready to receive traffic."},"503":{"description":"Service cannot safely receive traffic."}}}},"/trips/calculate":{"post":{"tags":["Trips"],"summary":"Calculate toll cost for a single trip","description":"Routes origin → destination (through optional waypoints), matches passage points, and prices each one. Returns an explainable per-line breakdown plus a total.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CalculateTripRequest"}}}},"responses":{"200":{"description":"Calculation completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripResultEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/trips/bulk":{"post":{"tags":["Trips"],"summary":"Calculate a batch of trips","description":"Synchronously calculates up to the plan's bulk limit of trips in one call.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["requests"],"properties":{"requests":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/CalculateTripRequest"}}}}}}},"responses":{"200":{"description":"Batch processed; see per-item results."},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/trips":{"get":{"tags":["Trips"],"summary":"List previously calculated trips","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Offset"},{"name":"vehicleId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"country","in":"query","schema":{"type":"string","enum":["NO","SE","DK","FI"]}},{"name":"source","in":"query","schema":{"type":"string","enum":["api","dashboard","bulk"]}},{"name":"departureFrom","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"departureTo","in":"query","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Paginated list of trips."},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/trips/{tripId}":{"get":{"tags":["Trips"],"summary":"Retrieve one trip with its full result","parameters":[{"name":"tripId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Trip with line items.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TripResultEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/vehicles":{"get":{"tags":["Vehicles"],"summary":"List vehicle profiles","responses":{"200":{"description":"Vehicle profiles for the tenant."},"401":{"$ref":"#/components/responses/Unauthorized"}}},"post":{"tags":["Vehicles"],"summary":"Create a vehicle profile","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VehicleInput"}}}},"responses":{"201":{"description":"Vehicle created."},"400":{"$ref":"#/components/responses/ValidationError"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/vehicles/{vehicleId}":{"get":{"tags":["Vehicles"],"summary":"Retrieve one vehicle profile","parameters":[{"name":"vehicleId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Vehicle profile."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"put":{"tags":["Vehicles"],"summary":"Update a vehicle profile","parameters":[{"name":"vehicleId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VehicleInput"}}}},"responses":{"200":{"description":"Vehicle updated."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Vehicles"],"summary":"Delete a vehicle profile","parameters":[{"name":"vehicleId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Vehicle deleted."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/toll-points":{"get":{"tags":["Reference"],"summary":"List passage points","parameters":[{"name":"country","in":"query","schema":{"type":"string","enum":["NO","SE","DK","FI"]}},{"name":"type","in":"query","schema":{"type":"string","enum":["toll","bridge","tunnel","ferry","zone","congestion"]}},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Offset"}],"responses":{"200":{"description":"Paginated toll points."},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/rates":{"get":{"tags":["Reference"],"summary":"List published rate rules","parameters":[{"name":"country","in":"query","schema":{"type":"string","enum":["NO","SE","DK","FI"]}},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Offset"}],"responses":{"200":{"description":"Paginated rate rules."},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/api-keys":{"get":{"tags":["Access"],"summary":"List API keys (admin)","responses":{"200":{"description":"API keys for the tenant."},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Access"],"summary":"Create an API key (admin)","description":"Returns the plaintext key once in `meta.plainTextKey`. SagaToll stores only a hash.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":80},"permissions":{"type":"array","items":{"type":"string","enum":["read","write","admin"]}},"rateLimit":{"type":"integer","minimum":1},"mode":{"type":"string","enum":["test","live"]}}}}}},"responses":{"201":{"description":"API key created."},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/api-keys/{apiKeyId}":{"delete":{"tags":["Access"],"summary":"Revoke an API key (admin)","parameters":[{"name":"apiKeyId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"API key revoked."},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Cannot revoke the last active admin key."}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Tenant API key. Prefix `st_live_` for production, `st_test_` for testing."}},"parameters":{"Limit":{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},"Offset":{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0}}},"responses":{"Unauthorized":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"Forbidden":{"description":"The API key lacks the required permission.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"NotFound":{"description":"The requested resource does not exist for this tenant.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"ValidationError":{"description":"The request body or query parameters failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"RateLimited":{"description":"Plan or key rate limit exceeded. See the `Retry-After` header.","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"schemas":{"ErrorEnvelope":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","example":"UNAUTHORIZED"},"message":{"type":"string"},"details":{}}},"meta":{"type":"object","properties":{"requestId":{"type":"string","format":"uuid"}}}}},"RoutePoint":{"type":"object","required":["latitude","longitude"],"properties":{"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180},"address":{"type":"string","nullable":true}}},"VehicleInput":{"type":"object","required":["type"],"properties":{"name":{"type":"string"},"registration":{"type":"string"},"type":{"type":"string","enum":["car","van","truck","truck_trailer","bus"]},"weightKg":{"type":"integer"},"axleCount":{"type":"integer","default":2},"euroClass":{"type":"string","enum":["euro3","euro4","euro5","euro6","euro6d","electric"]},"fuelType":{"type":"string","enum":["diesel","petrol","electric","hybrid","hydrogen"]},"hasAutopass":{"type":"boolean"},"hasBrobizz":{"type":"boolean"},"heightCm":{"type":"integer"},"lengthCm":{"type":"integer"}}},"CalculateTripRequest":{"type":"object","required":["origin","destination","vehicle","departureAt"],"properties":{"origin":{"$ref":"#/components/schemas/RoutePoint"},"destination":{"$ref":"#/components/schemas/RoutePoint"},"waypoints":{"type":"array","maxItems":25,"items":{"$ref":"#/components/schemas/RoutePoint"}},"route":{"type":"object","description":"Optional pre-computed route. When provided, SagaToll prices this exact geometry and skips its own routing — ideal when your TMS already planned the run. Send the full path, not just stops.","required":["coordinates"],"properties":{"coordinates":{"type":"array","minItems":2,"items":{"type":"object","required":["latitude","longitude"],"properties":{"latitude":{"type":"number"},"longitude":{"type":"number"}}}}}},"vehicle":{"oneOf":[{"type":"object","required":["id"],"properties":{"id":{"type":"string","format":"uuid"}},"description":"Reference an existing vehicle profile by id."},{"$ref":"#/components/schemas/VehicleInput"}]},"departureAt":{"type":"string","format":"date-time","description":"ISO 8601 with timezone offset, e.g. 2026-04-13T06:45:00+02:00."},"options":{"type":"object","properties":{"currency":{"type":"string","minLength":3,"maxLength":3},"includeRoute":{"type":"boolean","default":true},"includeAlternatives":{"type":"boolean","default":false}}}}},"Money":{"type":"object","properties":{"amount":{"type":"integer","description":"Minor units (øre/cent)."},"currency":{"type":"string"},"formatted":{"type":"string"}}},"TripResultEnvelope":{"type":"object","properties":{"data":{"type":"object","properties":{"tripId":{"type":"string","format":"uuid"},"status":{"type":"string","example":"completed"},"total":{"$ref":"#/components/schemas/Money"},"summary":{"type":"array","items":{"type":"object"}},"lineItems":{"type":"array","items":{"type":"object","properties":{"sequence":{"type":"integer"},"name":{"type":"string"},"type":{"type":"string"},"country":{"type":"string"},"amount":{"type":"integer"},"currency":{"type":"string"},"ruleApplied":{"type":"object"}}}},"route":{"type":"object"},"warnings":{"type":"array","items":{"type":"object"}},"metadata":{"type":"object"}}},"meta":{"type":"object"}}}}},"x-rate-limits":[{"plan":"free","requestsPerHour":100,"requestsPerDay":1000,"bulkSize":10},{"plan":"standard","requestsPerHour":1000,"requestsPerDay":25000,"bulkSize":100},{"plan":"premium","requestsPerHour":5000,"requestsPerDay":100000,"bulkSize":500},{"plan":"enterprise","requestsPerHour":10000,"requestsPerDay":250000,"bulkSize":1000}]}