{
  "openapi": "3.0.3",
  "info": {
    "title": "Ototo API",
    "version": "1",
    "description": "Belgian company-car taxation calculations, tax matrices, car details, and sharing endpoints. Uses the same routes and `subscription-key` authentication documented throughout this site. See the Authentication guide before making requests.",
    "contact": {
      "name": "Ototo"
    }
  },
  "servers": [
    { "url": "https://api.ototo.be/v1", "description": "Production" },
    { "url": "https://ototo-acc.azure-api.net/v1", "description": "Acceptance / sandbox" }
  ],
  "security": [ { "subscriptionKey": [] } ],
  "tags": [
    { "name": "Car Details", "description": "Create, update, and retrieve the vehicles you calculate against." },
    { "name": "Calculation", "description": "Run taxation calculations for a vehicle and profile." },
    { "name": "Sharing", "description": "Public sharing links and QR codes for a car detail." }
  ],
  "paths": {
    "/CarDetails/{carDetailId}": {
      "get": {
        "tags": ["Car Details"],
        "operationId": "GetCarDetail",
        "summary": "Retrieve a car detail",
        "parameters": [
          { "name": "carDetailId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The car detail's unique identifier" }
        ],
        "responses": {
          "200": {
            "description": "The car detail object",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CarDetail" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "description": "No car detail exists for that ID" },
          "500": { "description": "Something went wrong on the server. Retry later; contact Ototo if it persists." }
        }
      },
      "put": {
        "tags": ["Car Details"],
        "operationId": "UpsertCarDetail",
        "summary": "Update (or create) a car detail",
        "description": "Idempotent upsert at the given ID. Any `id` or `ownerId` in the body is ignored.",
        "parameters": [
          { "name": "carDetailId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The car detail's unique identifier" }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CarDetail" } } }
        },
        "responses": {
          "200": { "description": "Updated an existing car detail. A `Location` header points at the resource." },
          "201": { "description": "No car detail existed at that ID, so one was created. A `Location` header points at the new resource." },
          "400": { "description": "The car detail object was invalid — the response describes why" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "description": "Something went wrong on the server. Retry later; contact Ototo if it persists." }
        }
      }
    },
    "/CarDetails": {
      "post": {
        "tags": ["Car Details"],
        "operationId": "CreateCarDetail",
        "summary": "Create a car detail",
        "description": "Any `id` or `ownerId` sent in the body is ignored — both are assigned by Ototo.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CarDetail" } } }
        },
        "responses": {
          "201": { "description": "Created. A `Location` header points at the new resource." },
          "400": { "description": "The car detail object was invalid — the response describes why (missing or invalid data)" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "description": "Something went wrong on the server. Retry later; contact Ototo if it persists." }
        }
      }
    },
    "/Calculate": {
      "post": {
        "tags": ["Calculation"],
        "operationId": "Calculate",
        "summary": "Calculate taxation for a vehicle and profile",
        "description": "Requires calculation access to be enabled on your subscription, in addition to a valid key. Either `carDetailId` or `carDetail` must be provided; if both are given, `carDetail` takes precedence.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CalculationRequest" } } }
        },
        "responses": {
          "200": {
            "description": "Calculation completed successfully",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CalculationResult" } } }
          },
          "400": { "description": "The request body couldn't be parsed, `userProfile` or `options` is missing, `postalCode` is out of range, or the vehicle data is invalid" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "description": "`carDetailId` was provided but no matching car detail exists" }
        }
      }
    },
    "/GetTaxMatrix": {
      "get": {
        "tags": ["Calculation"],
        "operationId": "GetTaxMatrix",
        "summary": "RoadTax & YearlyTax matrix across every profile and region",
        "description": "Requires calculation access to be enabled on your subscription, in addition to a valid key. Newest endpoint on this list — confirm this exact route against your live gateway if it doesn't resolve; see the Tax Matrix reference page.",
        "parameters": [
          { "name": "id", "in": "query", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The car detail's unique identifier" }
        ],
        "responses": {
          "200": {
            "description": "Tax matrix calculated successfully",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TaxMatrixResult" } } }
          },
          "400": { "description": "`id` is missing or not a valid GUID" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "description": "No car detail found for the given `id`" }
        }
      }
    },
    "/PublicLink": {
      "get": {
        "tags": ["Sharing"],
        "operationId": "GetPublicLink",
        "summary": "Get a public, shareable URL for a car detail",
        "parameters": [
          { "name": "cardetailid", "in": "query", "required": true, "schema": { "type": "string", "format": "uuid" }, "description": "The car detail's unique identifier" }
        ],
        "responses": {
          "200": { "description": "A `Location` header containing the public URI for the requested car detail" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "description": "No car detail exists for that ID" },
          "500": { "description": "Something went wrong on the server. Retry later; contact Ototo if it persists." }
        }
      }
    },
    "/Qr": {
      "post": {
        "tags": ["Sharing"],
        "operationId": "GenerateQrCode",
        "summary": "Generate a QR code image for a car detail",
        "description": "Generated images are only available for 24–48 hours — download and store it yourself if you need it longer.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/QrRequest" } } }
        },
        "responses": {
          "201": { "description": "A `Location` header containing the URI of the generated QR image" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "description": "`carDetailId` not found" },
          "500": { "description": "Something went wrong on the server. Retry later; contact Ototo if it persists." }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "subscriptionKey": {
        "type": "apiKey",
        "in": "query",
        "name": "subscription-key",
        "description": "See the Authentication guide. Azure API Management also accepts this as an Ocp-Apim-Subscription-Key header."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Subscription key is missing or invalid, the subscription is inactive, or (for Calculate / Tax Matrix) calculation access isn't enabled"
      }
    },
    "schemas": {
      "CarDetail": {
        "type": "object",
        "required": ["catalogPrice", "fuel", "euroStandard", "kilowatt", "nedC2"],
        "properties": {
          "id": { "type": "string", "format": "uuid", "readOnly": true, "description": "Assigned by Ototo; ignored if sent on write" },
          "ownerId": { "type": "string", "format": "uuid", "readOnly": true, "description": "Assigned by Ototo; ignored if sent on write" },
          "name": { "type": "string", "description": "Display name shown to end users" },
          "internalName": { "type": "string", "description": "An internal identifier of your own choosing" },
          "catalogPrice": { "type": "number", "format": "decimal", "description": "Catalog price. Ototo stores prices excluding VAT unless your subscription is configured otherwise." },
          "firstRegistration": { "type": "string", "format": "date", "description": "Date of first registration. In practice required for vehicles registered on/after 2021-01-01, since wltp becomes mandatory from that date." },
          "fuel": { "$ref": "#/components/schemas/FuelType" },
          "euroStandard": { "$ref": "#/components/schemas/EuroStandard" },
          "kilowatt": { "type": "number", "format": "decimal", "description": "Engine power, in kW" },
          "wltp": { "type": "integer", "nullable": true, "description": "WLTP CO2 emissions. Mandatory for vehicles first registered on/after 2021-01-01." },
          "nedC2": { "type": "integer", "description": "NEDC2 CO2 emissions" },
          "capacity": { "type": "integer", "nullable": true, "description": "Engine capacity in cc. Required when fuel is Diesel, Petrol, LPG, NaturalGas, HydrogenPetrol, HybridPetrol, HybridDiesel, PluginHybridDiesel, or PluginHybridPetrol." },
          "weight": { "type": "integer", "nullable": true, "description": "Vehicle weight. Optional but recommended — used for false-hybrid detection and Wallonia road tax." },
          "batteryCapacity": { "type": "number", "format": "decimal", "nullable": true, "description": "Hybrid battery capacity. Required when fuel is PluginHybridDiesel or PluginHybridPetrol." },
          "nedC2NonHybrid": { "type": "integer", "nullable": true, "description": "NEDC2 emissions of the comparable non-hybrid version. Recommended for plug-in hybrids." },
          "wltpNonHybrid": { "type": "integer", "nullable": true, "description": "WLTP emissions of the comparable non-hybrid version. Recommended for plug-in hybrids." },
          "isActive": { "type": "boolean", "default": true, "description": "Leave as true. Reserved for future functionality." }
        }
      },
      "FuelType": {
        "type": "string",
        "enum": ["Diesel", "Petrol", "LPG", "NaturalGas", "PluginHybridDiesel", "PluginHybridPetrol", "HybridPetrol", "HybridDiesel", "Electric", "HydrogenPetrol", "Hydrogen"],
        "description": "Also accepts the numeric value instead of the name: Diesel=10, Petrol=20, LPG=25, NaturalGas=30, PluginHybridDiesel=40, PluginHybridPetrol=50, HybridPetrol=60, HybridDiesel=70, Electric=80, HydrogenPetrol=90, Hydrogen=100."
      },
      "EuroStandard": {
        "type": "string",
        "enum": ["NA", "Euro0", "Euro1", "Euro2", "Euro3", "Euro3Filter", "Euro4", "Euro4Filter", "Euro5", "Euro6", "Euro7"],
        "description": "Also accepts the numeric value instead of the name: NA=0, Euro0=10, Euro1=20, Euro2=30, Euro3=40, Euro3Filter=45, Euro4=50, Euro4Filter=55, Euro5=60, Euro6=70, Euro7=80."
      },
      "ProfileStatus": {
        "type": "string",
        "enum": ["Private", "SoleProprietor", "SelfEmployed"],
        "description": "Also accepts the numeric value instead of the name: Private=10, SoleProprietor=20, SelfEmployed=30."
      },
      "RegistrationOn": {
        "type": "string",
        "enum": ["PrivateRegistration", "CompanyRegistration", "LeasingCompanyRegistration"],
        "description": "Also accepts the numeric value instead of the name: PrivateRegistration=10, CompanyRegistration=20, LeasingCompanyRegistration=30."
      },
      "Region": {
        "type": "string",
        "enum": ["Flanders", "Wallonia", "Brussels"],
        "description": "Also accepts the numeric value instead of the name: Flanders=0, Wallonia=1, Brussels=2."
      },
      "ViewOptions": {
        "type": "integer",
        "description": "Bitmask of calculators to run. RoadTax=1, YearlyTax=2, BenefitInKind=4, FiscalDeduction=8, RejectedExpenses=16, CO2Contribution=32. Combine with bitwise OR — e.g. 63 runs every calculator.",
        "example": 63
      },
      "UserProfile": {
        "type": "object",
        "required": ["postalCode", "status", "registrationOn", "regionRegistration"],
        "properties": {
          "postalCode": { "type": "integer", "minimum": 1000, "maximum": 9992, "description": "Belgian postal code" },
          "status": { "$ref": "#/components/schemas/ProfileStatus" },
          "registrationOn": { "$ref": "#/components/schemas/RegistrationOn" },
          "regionRegistration": { "$ref": "#/components/schemas/Region" },
          "viewOptions": { "$ref": "#/components/schemas/ViewOptions" },
          "hasFuelCard": { "type": "boolean", "description": "Whether a fuel card is included" },
          "language": { "type": "string", "enum": ["nl", "fr", "en"], "description": "Result language" }
        }
      },
      "CalculationOptions": {
        "type": "object",
        "required": ["registrationDate"],
        "properties": {
          "registrationDate": { "type": "string", "format": "date", "description": "Intended registration date" },
          "orderDate": { "type": "string", "format": "date", "description": "Order/acquisition date, used for the fiscal deduction regime. Falls back to registrationDate if omitted." },
          "totalDiscount": { "type": "number", "format": "decimal", "nullable": true, "description": "Total dealer discount, incl. VAT" },
          "ownMonthlyCost": { "type": "number", "format": "decimal", "nullable": true, "description": "Employee's own monthly contribution" }
        }
      },
      "CalculationRequest": {
        "type": "object",
        "required": ["userProfile", "options"],
        "properties": {
          "carDetailId": { "type": "string", "format": "uuid", "nullable": true, "description": "Reference to a stored car detail. carDetailId or carDetail must be provided." },
          "carDetail": { "allOf": [{ "$ref": "#/components/schemas/CarDetail" }], "nullable": true, "description": "Inline vehicle object. Takes precedence over carDetailId if both are given." },
          "userProfile": { "$ref": "#/components/schemas/UserProfile" },
          "options": { "$ref": "#/components/schemas/CalculationOptions" },
          "version": { "type": "string", "description": "Client version tag, currently \"1\"; stored for auditing only" }
        }
      },
      "CalculationLine": {
        "type": "object",
        "properties": {
          "name": { "type": "string", "description": "Display name of the calculation, localised" },
          "description": { "type": "string", "nullable": true, "description": "Additional description, localised" },
          "viewOption": { "$ref": "#/components/schemas/ViewOptions" },
          "value": { "type": "string", "description": "Formatted display value, e.g. \"662.64€\"" },
          "rawValue": { "type": "number", "format": "decimal", "description": "Numeric value for programmatic use" },
          "year": { "type": "integer", "description": "Fiscal year the value applies to" },
          "url": { "type": "string", "nullable": true, "description": "Link to source legislation, when available" },
          "hasUrl": { "type": "boolean", "description": "Whether url is set" }
        }
      },
      "CalculationResult": {
        "type": "object",
        "properties": {
          "calculationId": { "type": "string", "format": "uuid", "description": "ID of the saved calculation request" },
          "lines": { "type": "array", "items": { "$ref": "#/components/schemas/CalculationLine" }, "description": "One entry per calculator that ran" }
        }
      },
      "TaxMatrixRegionEntry": {
        "type": "object",
        "properties": {
          "region": { "$ref": "#/components/schemas/Region" },
          "roadTax": { "type": "number", "format": "decimal", "nullable": true, "description": "Registration tax (BIV / TMC), excl. VAT. null if no result (e.g. an exemption applies)." },
          "yearlyTax": { "type": "number", "format": "decimal", "nullable": true, "description": "Annual road tax (verkeersbelasting), excl. VAT. null if no result." }
        }
      },
      "TaxMatrixRow": {
        "type": "object",
        "properties": {
          "status": { "$ref": "#/components/schemas/ProfileStatus" },
          "registrationOn": { "$ref": "#/components/schemas/RegistrationOn" },
          "regions": { "type": "array", "items": { "$ref": "#/components/schemas/TaxMatrixRegionEntry" } }
        }
      },
      "TaxMatrixResult": {
        "type": "object",
        "description": "A plain collection with no fixed property per region or entity type — iterate rows and regions dynamically rather than relying on array position.",
        "properties": {
          "carDetailId": { "type": "string", "format": "uuid" },
          "rows": { "type": "array", "items": { "$ref": "#/components/schemas/TaxMatrixRow" } }
        }
      },
      "QrRequest": {
        "type": "object",
        "required": ["carDetailId"],
        "properties": {
          "carDetailId": { "type": "string", "format": "uuid", "description": "The car detail's unique identifier" },
          "logoPath": { "type": "string", "default": "Ototo_QR_Code", "description": "The logo shown inside the QR code itself" },
          "qrCodeWidth": { "type": "integer", "default": 500, "description": "Width of the resulting image, in pixels" },
          "qrCodeHeight": { "type": "integer", "default": 750, "description": "Height of the resulting image, in pixels" },
          "captionLine1": { "type": "string", "nullable": true, "description": "First line of caption text below the QR code" },
          "captionLine2": { "type": "string", "nullable": true, "description": "Second line of caption text. Only used if captionLine1 is also set." },
          "captionColor": { "type": "string", "default": "000000", "description": "Caption text color, as a hex value without a leading #" },
          "captionTextSize": { "type": "integer", "default": 20, "description": "Caption font size" },
          "topLogoPath": { "type": "string", "nullable": true, "description": "Absolute path to a logo shown above the QR code" },
          "topLogoWidth": { "type": "integer", "default": 220, "description": "Width the top logo is resized to" },
          "topLogoHeight": { "type": "integer", "default": 110, "description": "Height the top logo is resized to" }
        }
      }
    }
  }
}
