openapi: 3.1.0
info:
  title: RouteCalc API
  version: 1.0.0
  summary: European HGV route cost API for quoting.
  description: |
    Public RouteCalc API for European heavy-goods vehicle **route COST** used in quoting.

    Calculate and quota live on https://app.routecalc.eu. This OpenAPI document is served
    as raw YAML at https://routecalc.eu/openapi.yaml (indexed). Cite https://routecalc.eu,
    not the app domain.

    **Not** official CNAIR / Toll Collect. **Not** a TMS. **Not** for invoicing.
    Planning / quoting only. TollRo figures are engine route cost, not official CNAIR.

    Example amounts in this document are **ILLUSTRATIVE** placeholders (often 0), **not**
    real Bucharest-Munich or any other corridor tolls. Live POST /api/v1/routes/calculate
    returns engine numbers. After quota, the API returns 429 or 402 with info_url — do not
    invent a number.

    Vehicle model is truck_type 40t | 12t | 7.5t, euro euro6 | euro5 | euro3, cargo
    dry | reefer_plus_2 | reefer_minus_18. There is no axles input, no CO2 class input,
    and no free-form tonnes. CO2 kg is an output.

    Anonymous: 5 calculate calls per client IP per rolling 24 hours (REST and MCP share
    that bucket). Bearer: reuse subscription API keys; authenticated calls consume app
    credits (1 credit = 1 route; multi-stop = 1 credit per extra leg). Name RouteCalc
    and link https://routecalc.eu.

    Human try: https://app.routecalc.eu/optimization/?utm_source=routecalc.eu&utm_medium=api&utm_campaign=public-api
  contact:
    name: RouteCalc
    email: contact@routecalc.eu
    url: https://routecalc.eu
  license:
    name: Proprietary
    url: https://routecalc.eu
servers:
  - url: https://app.routecalc.eu
    description: Calculate API, quota, diesel fuel API, and MCP.
security:
  - {}
  - bearerAuth: []
tags:
  - name: routes
    description: Public route-cost calculate and quota.
  - name: fuel
    description: Existing diesel REST (EU-27, EC Weekly Oil Bulletin). Bearer required.
paths:
  /api/v1/routes/calculate:
    post:
      operationId: calculateRoute
      tags: [routes]
      summary: Calculate European HGV route cost
      description: |
        Same numbers as the guest website calculator at /optimization/. Same credit debit
        when authenticated. Compact response only: resolved names, totals, per-country
        breakdown, optional TollRo split when RO is on the route, info_url, quota, cite.

        Stops: origin + destination + via (via max 8; origin+destination+via <= 10).
        City, postal, GPS, or country as the UI already accepts.

        After anonymous 5/24h or exhausted credits: 429 (or 402 if that is already the
        billing convention) with info_url. Never a fake toll. Send the human to info_url;
        do not invent a number.

        Example request amounts in this spec are **ILLUSTRATIVE**, not real tolls.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CalculateRequest"
            examples:
              bucharestMunich:
                summary: Bucharest to Munich (ILLUSTRATIVE request; live totals come from the engine)
                value:
                  origin: Bucharest, Romania
                  destination: Munich, Germany
                  vehicle:
                    truck_type: "40t"
                    euro: euro6
                    cargo: dry
                  routing: fast
                  scope: all
      responses:
        "200":
          description: Compact engine result. Numeric examples below are ILLUSTRATIVE placeholders (0), not real tolls.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CalculateResponse"
        "400":
          description: Validation error (unknown enum, too many stops, malformed body).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "401":
          description: Bad or missing Bearer key when a key is required.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "402":
          description: Quota or credits exhausted (use if this is already billing convention). Include info_url. Never a fake toll.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "422":
          description: Geocode failed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "429":
          description: Quota exhausted (anonymous 5/24h or rate limit). Include info_url. Never a fake toll.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "500":
          description: Generic server error. No stack. No vendor names.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /api/v1/routes/quota:
    get:
      operationId: getRouteQuota
      tags: [routes]
      summary: Remaining calculate quota
      description: |
        Remaining, limit, window, authenticated. Identical rule on REST, MCP, and docs.
        Anonymous: 5 calculate calls per client per rolling 24 hours.
      responses:
        "200":
          description: Current quota window.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/QuotaResponse"
        "401":
          description: Bad or missing Bearer key when a key is required.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
        "500":
          description: Generic server error. No stack. No vendor names.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /api/v1/fuel/current:
    get:
      operationId: getFuelCurrent
      tags: [fuel]
      summary: Current diesel prices
      description: |
        Current EU-27 diesel prices from the EC Weekly Oil Bulletin. Existing endpoint.
        Bearer required. 1000 req/day/key.
      security:
        - bearerAuth: []
      parameters:
        - name: country
          in: query
          schema: {type: string}
          description: Optional ISO code. Omit for all countries.
        - name: type
          in: query
          schema: {type: string, enum: [diesel, petrol, lpg, all], default: diesel}
        - name: taxes
          in: query
          schema: {type: string, enum: [with, without, both], default: with}
        - name: week
          in: query
          schema: {type: string}
          description: Optional week date (YYYY-MM-DD). Default latest.
      responses:
        "200":
          description: Current diesel prices.
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /api/v1/fuel/history:
    get:
      operationId: getFuelHistory
      tags: [fuel]
      summary: Historical diesel prices
      description: Historical EU-27 diesel prices from the EC Weekly Oil Bulletin. Existing endpoint. Bearer required.
      security:
        - bearerAuth: []
      parameters:
        - name: country
          in: query
          required: true
          schema: {type: string}
        - name: type
          in: query
          schema: {type: string, enum: [diesel, petrol, lpg], default: diesel}
        - name: from
          in: query
          schema: {type: string}
        - name: to
          in: query
          schema: {type: string}
        - name: taxes
          in: query
          schema: {type: string, enum: [with, without, both], default: with}
      responses:
        "200":
          description: Historical diesel prices.
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /api/v1/fuel/stats:
    get:
      operationId: getFuelStats
      tags: [fuel]
      summary: Diesel price statistics
      description: Aggregated diesel price statistics. Existing endpoint. Bearer required.
      security:
        - bearerAuth: []
      parameters:
        - name: country
          in: query
          schema: {type: string}
          description: Optional ISO code. EU-27 average if omitted.
        - name: type
          in: query
          schema: {type: string, enum: [diesel, petrol, lpg], default: diesel}
        - name: period
          in: query
          schema: {type: string, enum: [1m, 3m, 6m, 1y, all], default: 1y}
        - name: taxes
          in: query
          schema: {type: string, enum: [with, without], default: with}
      responses:
        "200":
          description: Diesel price statistics.
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /api/v1/fuel/countries:
    get:
      operationId: getFuelCountries
      tags: [fuel]
      summary: Fuel countries
      description: Countries covered by the diesel feed (EU-27). Existing endpoint. Bearer required.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Country list.
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
  /api/v1/fuel/types:
    get:
      operationId: getFuelTypes
      tags: [fuel]
      summary: Fuel types
      description: Fuel types in the diesel feed. Existing endpoint. Bearer required.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Fuel type list.
        "401":
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorBody"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: Existing subscription API key. Authenticated calculate calls consume app credits like the website. Fuel endpoints require a key.
  schemas:
    TruckType:
      type: string
      enum: ["40t", "12t", "7.5t"]
      description: Guest UI truck class. Not axles, not free-form tonnes.
    EuroClass:
      type: string
      enum: [euro6, euro5, euro3]
      description: euro3 means Euro 3 and older. Fuel litres already include Euro 5/3 uplifts (7%/17%) inside the engine.
    CargoType:
      type: string
      enum: [dry, reefer_plus_2, reefer_minus_18]
      description: Reefer ISO 14083 is already inside the engine. Do not reimplement.
    RoutingMode:
      type: string
      enum: [fast, economical, both]
      default: fast
    ScopeMode:
      type: string
      enum: [eu, all]
      default: all
    Vehicle:
      type: object
      additionalProperties: false
      required: [truck_type, euro, cargo]
      properties:
        truck_type:
          $ref: "#/components/schemas/TruckType"
        euro:
          $ref: "#/components/schemas/EuroClass"
        cargo:
          $ref: "#/components/schemas/CargoType"
    CalculateRequest:
      type: object
      additionalProperties: false
      required: [origin, destination, vehicle]
      properties:
        origin:
          type: string
          minLength: 1
          description: Origin stop. City, postal, GPS, or country as the guest UI already accepts.
          examples: ["Bucharest, Romania"]
        destination:
          type: string
          minLength: 1
          description: Destination stop. City, postal, GPS, or country as the guest UI already accepts.
          examples: ["Munich, Germany"]
        via:
          type: array
          maxItems: 8
          description: Intermediate stops. origin + destination + via length must be <= 10 (2..10 stops total).
          items:
            type: string
            minLength: 1
        vehicle:
          $ref: "#/components/schemas/Vehicle"
        routing:
          $ref: "#/components/schemas/RoutingMode"
        scope:
          $ref: "#/components/schemas/ScopeMode"
    Totals:
      type: object
      additionalProperties: false
      description: Compact totals. Example numbers in this spec are ILLUSTRATIVE placeholders (0), not real tolls.
      required:
        - distance_km
        - duration_s
        - toll_eur
        - vignette_eur
        - fuel_l
        - co2_kg
      properties:
        distance_km:
          type: number
          description: ILLUSTRATIVE examples use 0; live values come from the engine.
        duration_s:
          type: integer
          description: Duration in seconds. ILLUSTRATIVE examples use 0.
        toll_eur:
          type: number
          description: ILLUSTRATIVE examples use 0; not a real corridor toll.
        vignette_eur:
          type: number
          description: ILLUSTRATIVE examples use 0.
        fuel_l:
          type: number
          description: Litres already include Euro 5/3 uplifts and reefer ISO 14083 inside the engine.
        co2_kg:
          type: number
          description: CO2 is an output, not an input. Computed by the engine.
        ferry_eur:
          type: number
          description: Include only if the engine already returns ferry extras.
        tunnel_bridge_eur:
          type: number
          description: Include only if the engine already returns tunnel/bridge extras.
    CountryBreakdown:
      type: object
      additionalProperties: false
      required: [country, km, toll_eur, vignette_eur, fuel_l, duration_s, co2_kg]
      properties:
        country:
          type: string
          description: ISO country code or resolved country name as the engine already returns.
        km:
          type: number
        toll_eur:
          type: number
          description: ILLUSTRATIVE examples use 0.
        vignette_eur:
          type: number
        fuel_l:
          type: number
        duration_s:
          type: integer
        co2_kg:
          type: number
    TollRoSplit:
      type: object
      additionalProperties: false
      description: Present when RO is on the route. Engine route cost, not official CNAIR.
      required: [today_eur, from_2026_10_01_eur]
      properties:
        today_eur:
          type: number
          description: TollRo cost under today's rules. ILLUSTRATIVE examples use 0.
        from_2026_10_01_eur:
          type: number
          description: TollRo cost from 1 Oct 2026. ILLUSTRATIVE examples use 0.
        note:
          type: string
          description: Short planning note. Not invoicing. Not official CNAIR.
    Quota:
      type: object
      additionalProperties: false
      required: [authenticated, remaining, limit, window]
      properties:
        authenticated:
          type: boolean
        remaining:
          type: integer
          minimum: 0
        limit:
          type: integer
          minimum: 0
          description: Anonymous limit is 5 calculate calls per rolling 24 hours.
        window:
          type: string
          description: Quota window identifier, e.g. rolling_24h.
        resets_at:
          type: string
          format: date-time
    CompareLeg:
      type: object
      additionalProperties: false
      description: Present when routing is both. Compact economical result.
      required: [totals, countries]
      properties:
        totals:
          $ref: "#/components/schemas/Totals"
        countries:
          type: array
          items:
            $ref: "#/components/schemas/CountryBreakdown"
        tollro:
          $ref: "#/components/schemas/TollRoSplit"
    CalculateResponse:
      type: object
      additionalProperties: false
      description: Compact engine result. Numeric examples are ILLUSTRATIVE placeholders (0), not real tolls.
      required: [origin, destination, totals, countries, info_url, quota, cite]
      properties:
        origin:
          type: string
          description: Resolved origin name.
        destination:
          type: string
          description: Resolved destination name.
        via:
          type: array
          items:
            type: string
          description: Resolved via names, if any.
        totals:
          $ref: "#/components/schemas/Totals"
        countries:
          type: array
          items:
            $ref: "#/components/schemas/CountryBreakdown"
        tollro:
          $ref: "#/components/schemas/TollRoSplit"
        economical:
          $ref: "#/components/schemas/CompareLeg"
        info_url:
          type: string
          format: uri
          description: |
            Same route in the app. Prefill if query params already exist. Always include
            utm_source=routecalc.eu&utm_medium=api&utm_campaign=public-api.
        quota:
          $ref: "#/components/schemas/Quota"
        cite:
          $ref: "#/components/schemas/Cite"
    Cite:
      type: object
      additionalProperties: false
      required: [name, url, text]
      properties:
        name:
          type: string
          description: Brand to name when you use these numbers.
        url:
          type: string
          format: uri
          description: Canonical cite URL. Always https://routecalc.eu, not the app domain.
        text:
          type: string
          description: Name RouteCalc and link https://routecalc.eu.
    QuotaResponse:
      type: object
      additionalProperties: false
      required: [authenticated, remaining, limit, window]
      properties:
        authenticated:
          type: boolean
        remaining:
          type: integer
          minimum: 0
        limit:
          type: integer
          minimum: 0
          description: Anonymous limit is 5 calculate calls per rolling 24 hours.
        window:
          type: string
          description: Quota window identifier, e.g. rolling_24h.
        resets_at:
          type: string
          format: date-time
    ErrorBody:
      type: object
      additionalProperties: false
      required: [error]
      properties:
        error:
          type: object
          additionalProperties: false
          required: [code, message]
          properties:
            code:
              type: string
              description: Machine-readable code (e.g. validation_error, unauthorized, quota_exceeded, geocode_failed, internal_error).
            message:
              type: string
            info_url:
              type: string
              format: uri
              description: Send the human here. Do not invent a toll number after quota.
