openapi: "3.1.0"
info:
  title: bodenrichtwert.ai API
  description: |
    API for retrieving official German land values (Bodenrichtwerte) by address.
    Provide a full German address and receive the land value in EUR per square meter,
    including the land use type, reference date, municipality, and source attribution.
    Covers all German states except Schleswig-Holstein, Sachsen, and Bayern (blocked for legal reasons, HTTP 451).
  version: "1.1.0"
  contact:
    url: https://api.bodenrichtwert.ai

servers:
  - url: https://api.bodenrichtwert.ai
    description: Production REST API

paths:
  /api/bodenrichtwert:
    get:
      operationId: getBodenrichtwert
      summary: Get the official land value (Bodenrichtwert) for a German address
      description: |
        Returns official land values (Bodenrichtwerte) in EUR per square meter for a given German address.

        Usage:
        - Pass a full German address as the "address" parameter (e.g. "Marienplatz 1, 80331 München" or "Unter den Linden 1, Berlin").
        - Without "nutzungsart", every land use type found at the address is returned (W residential, M mixed, G commercial, S special, L agricultural, F forestry, SO other).
        - Pass "nutzungsart" one or more times to filter to specific codes, e.g. "?nutzungsart=W&nutzungsart=G".
        - Breaking change (2026-04): the legacy code "LF" has been removed — clients must migrate to "L" and/or "F".

        Restrictions:
        - Schleswig-Holstein (SH), Sachsen (SN), and Bayern (BY) are legally excluded and return HTTP 451.
        - A valid Bearer token from OAuth authentication is required.
      parameters:
        - name: address
          in: query
          required: true
          description: Full German address including street, house number, and city (e.g. "Friedrichstraße 43, 10117 Berlin")
          schema:
            type: string
          example: "Unter den Linden 1, Berlin"
        - name: nutzungsart
          in: query
          required: false
          description: |
            Filter by land use type (official land-use category code). Repeat the parameter to filter
            by multiple codes at once, e.g. `?nutzungsart=W&nutzungsart=G` (not comma-separated):
            - W  = Wohnbaufläche (residential)
            - M  = Gemischte Baufläche (mixed residential/commercial)
            - G  = Gewerbliche Baufläche (commercial/industrial)
            - S  = Sonderbaufläche (special, e.g. hospitals, schools)
            - L  = Landwirtschaftliche Fläche (agricultural)
            - F  = Forstwirtschaftliche Fläche (forestry)
            - SO = Sonstige Fläche (other)

            Breaking change: the previous combined code `LF` has been removed — land and
            forestry are now separate codes (`L`, `F`), matching how the official source
            separates agricultural and forestry land. Clients that previously queried `LF` must migrate to `L` and/or `F`.

            If omitted, no filter is applied — every land use type found at the address is
            returned in `bodenrichtwerte`, sorted W, M, G, S, L, F, SO.
          schema:
            type: array
            items:
              type: string
              enum: [W, M, G, S, L, F, SO]
          style: form
          explode: true
      security:
        - OAuth2: []
        - BearerAuth: []
      responses:
        "200":
          description: Land value(s) found for the given address
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BodenrichtwertResponse"
              example:
                address_input: "Prenzlauer Allee 200, Berlin"
                address_resolved: "Prenzlauer Allee 200, 10405 Berlin - Prenzlauer Berg"
                coordinates:
                  lat: 52.541454
                  lon: 13.412894
                bodenrichtwerte:
                  - nutzungsart: "Wohnbaufläche"
                    code: "W"
                    brw_euro_pro_m2: 5000
                    stichtag: "2026-01-01"
                    gemeinde: "Berlin"
                    gutachterausschuss: "Gutachterausschuss für Grundstückswerte Berlin"
                    bodenrichtwertnummer: "0405W005"
                    zonenbezeichnung: "Prenzlauer Berg"
                  - nutzungsart: "Gemischte Baufläche"
                    code: "M"
                    brw_euro_pro_m2: 4200
                    stichtag: "2026-01-01"
                    gemeinde: "Berlin"
                    gutachterausschuss: "Gutachterausschuss für Grundstückswerte Berlin"
                    bodenrichtwertnummer: "0405M003"
                    zonenbezeichnung: "Prenzlauer Berg"
                cache_hit: false
                attribution:
                  text: "© Daten der Gutachterausschüsse für Grundstückswerte 2026, dl-de/by-2-0"
                  license_url: "https://www.govdata.de/dl-de/by-2-0"
                  source_url: "https://www.bodenrichtwerte-boris.de"
                  year: 2026
                disclaimer: "Amtlicher Bodenrichtwert gemäß dl-de/by-2-0. Gutachterlicher Orientierungswert — kein Verkehrswert, kein Verkaufspreis, keine Rechtsberatung. Stichtag pro Eintrag beachten."
        "400":
          description: Missing or invalid parameters
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Missing or invalid Bearer token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "402":
          description: |
            Request blocked for billing reasons. Error code "limit_reached" = request
            quota exhausted — `reason` says why ("trial_exhausted" = 100-request trial
            cap used up, "no_active_plan" = trial over and no plan booked) and
            `upgrade_url` deep-links to the billing page. "payment_past_due" = the last
            payment failed and the grace period has elapsed (update payment details).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Address not found or no land value data available for this location
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: IP rate limit exceeded (120 requests/minute) — retry after the number of seconds in the Retry-After header
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "451":
          description: This state (Schleswig-Holstein, Sachsen, or Bayern) is legally excluded and provides no land-value data
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Unexpected internal error — retry later; contact support@bodenrichtwert.ai if it persists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "503":
          description: Official land-value data source temporarily unavailable
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/bodenwert:
    post:
      operationId: calculateBodenwert
      summary: Calculate the Bodenwert (density-adjusted land value) for a plot
      description: |
        Calculates the actual land value of a plot at a building density (Geschossflächenzahl, GFZ)
        that differs from the one the official Bodenrichtwert is based on. Pure calculation — no address
        lookup, no external data-source call — and it does NOT count against your monthly request quota.

        Without `flaeche` it returns the actual Bodenrichtwert (density-adjusted, EUR/m²); with `flaeche`
        it additionally returns the Bodenwert (total plot value, EUR).

        Model: the standard German conversion-coefficient formula, used by the Gutachterausschüsse as
        the fallback when no zone-specific table is published (cf. ErbStH on § 179 BewG):
        - K(GFZ) = 0.6·√GFZ + 0.2·GFZ + 0.2   (normalized so K(1.0) = 1.0)
        - bodenrichtwert_tatsaechlich = brw × K(gfz_tatsaechlich) / K(gfz_referenz)

        The reference GFZ (`gfz_referenz`) is the density the Bodenrichtwert is calibrated for — from
        `GET /api/bodenrichtwert` it is the `wertrel_geschossflaechenzahl` field. Not every state
        publishes it (e.g. NRW exposes only the number of full storeys); then supply the reference GFZ
        from the official zone information yourself.

        The result is an orienting calculation and does NOT replace the zone-specific official conversion
        table of the responsible Gutachterausschuss. It is not a market value.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [brw, gfz_referenz, gfz_tatsaechlich]
              properties:
                brw:
                  type: number
                  minimum: 0
                  description: The official Bodenrichtwert to base the calculation on, in EUR per square meter.
                gfz_referenz:
                  type: number
                  exclusiveMinimum: 0
                  description: Reference GFZ the Bodenrichtwert is based on (wertrelevante GFZ, greater than 0).
                gfz_tatsaechlich:
                  type: number
                  exclusiveMinimum: 0
                  description: Actual or permitted GFZ of the subject plot (greater than 0).
                flaeche:
                  type: number
                  exclusiveMinimum: 0
                  description: Optional plot area in square meters. If given, the total Bodenwert is also returned.
            example:
              brw: 450
              gfz_referenz: 0.8
              gfz_tatsaechlich: 1.2
      security:
        - OAuth2: []
        - BearerAuth: []
      responses:
        "200":
          description: The density-adjusted land value (and total Bodenwert if flaeche was given)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BodenwertResponse"
              example:
                brw_input_euro_pro_m2: 450
                gfz_referenz: 0.8
                gfz_tatsaechlich: 1.2
                koeffizient_referenz: 0.8967
                koeffizient_tatsaechlich: 1.0973
                faktor: 1.2237
                bodenrichtwert_tatsaechlich_euro_pro_m2: 550.68
                modell: amtliche-standardformel
                formel: "K(GFZ) = 0,6·√GFZ + 0,2·GFZ + 0,2  ·  BRW_tatsächlich = BRW × K(GFZ_ist) / K(GFZ_ref)"
                disclaimer: "Orientierende Umrechnung nach der Standardformel für Umrechnungskoeffizienten (0,6·√GFZ + 0,2·GFZ + 0,2; u. a. ErbStH zu § 179 BewG). Ersetzt NICHT die zonenspezifische amtliche Umrechnungstabelle des zuständigen Gutachterausschusses. Gutachterlicher Orientierungswert — kein Verkehrswert, kein Kaufpreis, keine Rechtsberatung."
        "400":
          description: Missing or invalid fields (malformed JSON body, non-numeric value, brw < 0, or a GFZ <= 0)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Missing or invalid Bearer token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: IP rate limit exceeded (120 requests/minute) — retry after the number of seconds in the Retry-After header
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Unexpected internal error — retry later; contact support@bodenrichtwert.ai if it persists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/me:
    get:
      operationId: getUserProfile
      summary: Get your profile and current usage statistics
      description: |
        Returns your user ID, subscription plan (free, starter, pro, or enterprise), your current
        usage, your monthly hard cap, and the state of your 14-day Pro trial.
        Useful for checking remaining quota before making requests.
      security:
        - OAuth2: []
        - BearerAuth: []
      responses:
        "200":
          description: User profile and usage data
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    description: Your user ID
                  plan:
                    type: string
                    enum: [free, starter, pro, enterprise]
                    description: Your booked subscription plan
                  effective_plan:
                    type: string
                    enum: [free, starter, pro, enterprise]
                    description: The plan currently in effect — "pro" while the 14-day trial is active, otherwise same as plan
                  usage_this_month:
                    type: integer
                    description: Number of API requests counted this month
                  limit:
                    type: [integer, "null"]
                    description: Effective hard cap this month — 100 during the trial, 0 on free (paywall), 50 on starter, 500 on pro, null on enterprise (unlimited by contract). Requests exceeding the cap are rejected with 402 limit_reached.
                  quota_exhausted:
                    type: boolean
                    description: true when the next lookup would be blocked with 402 limit_reached
                  upgrade_url:
                    type: [string, "null"]
                    description: Deep link to the billing page when quota_exhausted, otherwise null
                  trial_expires_at:
                    type: [string, "null"]
                    format: date-time
                    description: When the 14-day Pro trial ends (ISO 8601), or null if no trial
                  trial_usage:
                    type: integer
                    description: Requests used during the trial window
                  trial_limit:
                    type: integer
                    description: Total request cap for the entire trial window (currently 100)
                  trial_active:
                    type: boolean
                    description: true while the trial is running and not exhausted
        "401":
          description: Missing or invalid Bearer token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "500":
          description: Unexpected internal error — retry later; contact support@bodenrichtwert.ai if it persists
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

components:
  securitySchemes:
    OAuth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://bodenrichtwert.ai/oauth/authorize
          tokenUrl: https://bodenrichtwert.ai/oauth/token
          scopes: {}
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        Static API key issued in the dashboard (Entwickler → API-Keys). Pass as
        `Authorization: Bearer <key>`. Alternative to the OAuth2 flow above —
        recommended for direct server-to-server integrations that skip the
        interactive OAuth consent screen.

  schemas:
    BodenrichtwertResponse:
      type: object
      properties:
        address_input:
          type: string
          description: The original address you provided
        address_resolved:
          type: string
          description: The geocoded, normalized address
        coordinates:
          type: object
          properties:
            lat:
              type: number
              description: Latitude (WGS84)
            lon:
              type: number
              description: Longitude (WGS84)
        bodenrichtwerte:
          type: array
          description: List of land values at this location (may contain multiple entries for different land use types)
          items:
            $ref: "#/components/schemas/Bodenrichtwert"
        cache_hit:
          type: boolean
          description: Whether the result was served from cache
        attribution:
          $ref: "#/components/schemas/Attribution"
        disclaimer:
          type: string
          description: |
            Pflicht-Hinweis, dass der Bodenrichtwert ein gutachterlicher Orientierungswert ist —
            kein Verkehrswert, kein Verkaufspreis, keine Rechtsberatung. Muss bei Weitergabe sichtbar bleiben.

    Bodenrichtwert:
      type: object
      properties:
        nutzungsart:
          type: string
          description: "Human-readable land use type in German (e.g. Wohnbaufläche, Gewerbliche Baufläche)"
        code:
          type: string
          enum: [W, M, G, S, L, F, SO]
          description: "Short code for the official land-use category: W=residential, M=mixed, G=commercial, S=special, L=agricultural, F=forestry, SO=other"
        brw_euro_pro_m2:
          type: number
          description: Land value in EUR per square meter
        stichtag:
          type: string
          format: date
          description: Reference date of the land value assessment (ISO 8601, YYYY-MM-DD)
        gemeinde:
          type: string
          description: Municipality name
        gutachterausschuss:
          type: string
          description: |
            Name of the expert committee (Gutachterausschuss) that issued the value.
            May be an empty string in some federal states where the official source does not
            publish the committee name in its feed (only the numeric committee ID).
        bodenrichtwertnummer:
          type: string
          description: |
            Unique identifier of the BRW zone within its Gutachterausschuss. Use this
            when referencing a specific zone in a formal appraisal — the value is
            stable across requests for the same coordinate.
        zonenbezeichnung:
          type: string
          description: |
            Human-readable name of the BRW zone — often a district or quarter
            name (e.g. "Regierungsviertel" for the government quarter in Hannover),
            optionally combined with the land use code (e.g. "Stederdorf W" for the
            Wohnbau zone in the Stederdorf district).
        bauweise:
          type: string
          description: |
            Permitted building configuration as published in the land development plan
            (e.g. "offen" = detached, "geschlossen" = attached). Often unset.
        geschossflaechenzahl:
          type: number
          description: |
            Floor Area Ratio (GFZ) permitted in the zone — the ratio of total gross
            floor area to plot area. Input for the price conversion if the subject
            plot has a different FAR than the one the BRW was calibrated for.
        grundflaechenzahl:
          type: number
          description: |
            Site Coverage Ratio (GRZ) — the ratio of built-over area to plot area.
        vollgeschosszahl:
          type: number
          description: Number of full storeys (VGZ) permitted in the zone.
        baumassenzahl:
          type: number
          description: |
            Building Mass Ratio (BMZ) — gross building volume per plot area. Used
            primarily in dense commercial / high-rise zones instead of GFZ.
        wertrel_geschossflaechenzahl:
          type: number
          description: |
            Reference Floor Area Ratio ("wertrelevante GFZ", WGF) — the GFZ the
            expert committee used as the basis for this Bodenrichtwert. If the
            subject plot has a different GFZ, the BRW must be re-indexed against
            the committee's published conversion table. Without this field a
            professional appraisal cannot adjust the value accurately.

    BodenwertResponse:
      type: object
      properties:
        brw_input_euro_pro_m2:
          type: number
          description: The official Bodenrichtwert you provided (EUR/m²).
        gfz_referenz:
          type: number
          description: Reference GFZ the value is based on (wertrelevante GFZ).
        gfz_tatsaechlich:
          type: number
          description: Actual or permitted GFZ of the subject plot.
        koeffizient_referenz:
          type: number
          description: "Conversion coefficient K(gfz_referenz) = 0.6·√GFZ + 0.2·GFZ + 0.2."
        koeffizient_tatsaechlich:
          type: number
          description: "Conversion coefficient K(gfz_tatsaechlich)."
        faktor:
          type: number
          description: "Conversion factor K(gfz_tatsaechlich) / K(gfz_referenz)."
        bodenrichtwert_tatsaechlich_euro_pro_m2:
          type: number
          description: The actual (density-adjusted) Bodenrichtwert for the plot (EUR/m²).
        grundstuecksflaeche_m2:
          type: number
          description: Plot area — present only when `flaeche` was supplied.
        bodenwert_euro:
          type: number
          description: The Bodenwert (total plot value) = bodenrichtwert_tatsaechlich × flaeche — present only when `flaeche` was supplied.
        modell:
          type: string
          enum: [amtliche-standardformel]
          description: The calculation model used.
        formel:
          type: string
          description: Human-readable description of the applied formula.
        hinweis:
          type: string
          description: Optional note, e.g. an extrapolation warning when a GFZ is outside the usual range.
        disclaimer:
          type: string
          description: |
            Mandatory note that this is an orienting conversion using the standard formula — not the
            zone-specific official conversion table, and not a market value.
      required:
        - brw_input_euro_pro_m2
        - gfz_referenz
        - gfz_tatsaechlich
        - koeffizient_referenz
        - koeffizient_tatsaechlich
        - faktor
        - bodenrichtwert_tatsaechlich_euro_pro_m2
        - modell
        - formel
        - disclaimer

    Attribution:
      type: object
      description: Required attribution for the data source (dl-de/by-2-0 license)
      properties:
        text:
          type: string
        license_url:
          type: string
          format: uri
        source_url:
          type: string
          format: uri
        year:
          type: integer

    Error:
      type: object
      properties:
        error:
          type: string
          description: Error code
        message:
          type: string
          description: Human-readable error description
        status:
          type: integer
          description: HTTP status code
        reason:
          type: string
          enum: [trial_exhausted, no_active_plan]
          description: Only on 402 limit_reached — why the quota is exhausted
        upgrade_url:
          type: string
          description: Only on 402 limit_reached — deep link to the billing page to book a plan
