openapi: 3.1.0
info:
  title: EGB Eiendom API
  version: 1.0.0
  summary: Boliger og lokaler fra EGB – til leie, til salgs og kommende.
  description: |
    Versjonert API for EGBs nettside og fremtidig eiendomsdriftssystem.

    **Sikkerhetsmodell**
    - Offentlige endepunkter (`/v1/units*`) er kun lesing, inneholder ingen personopplysninger,
      caches (ETag + Cache-Control) og har CORS begrenset til EGBs egne domener.
    - Skjemainnsending (`/v1/units/{slug}/interest`, `/v1/units/{slug}/bookings`) skjer server-side
      med validering, samtykke, bot-beskyttelse (captcha-token) og rate limiting per IP.
      Svarene bekrefter bare mottak og returnerer aldri lagrede personopplysninger.
    - Skriving av boligdata og webhooks er kun system-til-system fra eiendomsdriftssystemet:
      OAuth2 client credentials (scope `units:write`) **og** HMAC-SHA256-signatur over rå request-body
      i `X-EGB-Signature`, med `X-EGB-Timestamp` (maks 5 min avvik) for å hindre replay.
    - Alle skrivende kall krever `Idempotency-Key`.
    - Ingen hemmeligheter i frontend. Nøkler roteres og lagres i hemmelighetshvelv.
    - Kun TLS 1.2+. Feil returneres som RFC 9457 Problem Details.
    - Brytende endringer gir ny hovedversjon (`/v2`). `/v1` varsles med `Deprecation`/`Sunset`-headere minst 6 måneder før utfasing.

    **Prototype:** nettsiden leser i dag den statiske filen `api/v1/units.json` med samme format som
    `UnitList`. Serveren bygges ved lansering.
  contact:
    name: EGB Enterprises AS
servers:
  - url: https://api.egb.no
    description: Produksjon (planlagt)
  - url: https://api.staging.egb.no
    description: Test (planlagt)
tags:
  - name: Boliger
    description: Offentlig, kun lesing
  - name: Henvendelser
    description: Interesse og visning fra publikum
  - name: Integrasjon
    description: System-til-system fra eiendomsdrift
paths:
  /v1/units:
    get:
      tags: [Boliger]
      operationId: listUnits
      summary: List boliger og lokaler
      parameters:
        - name: status
          in: query
          schema: { $ref: '#/components/schemas/Status' }
        - name: listing
          in: query
          schema: { type: string, enum: [leie, salg] }
        - name: city
          in: query
          schema: { type: string, maxLength: 60 }
        - name: cursor
          in: query
          schema: { type: string, maxLength: 200 }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
      responses:
        '200':
          description: OK
          headers:
            ETag: { schema: { type: string } }
            Cache-Control: { schema: { type: string, example: 'public, max-age=60, stale-while-revalidate=300' } }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/UnitList' }
        '304': { description: Ikke endret }
        '429': { $ref: '#/components/responses/TooManyRequests' }
  /v1/units/{slug}:
    get:
      tags: [Boliger]
      operationId: getUnit
      summary: Hent én bolig
      parameters: [ { $ref: '#/components/parameters/Slug' } ]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Unit' }
        '404': { $ref: '#/components/responses/NotFound' }
  /v1/units/{slug}/interest:
    post:
      tags: [Henvendelser]
      operationId: registerInterest
      summary: Meld interesse eller be om prospekt
      description: Kun tilgjengelig når `interest.enabled` er true for boligen.
      parameters:
        - { $ref: '#/components/parameters/Slug' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/InterestRequest' }
      responses:
        '202': { $ref: '#/components/responses/Accepted' }
        '409': { $ref: '#/components/responses/Disabled' }
        '422': { $ref: '#/components/responses/Invalid' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
  /v1/units/{slug}/bookings:
    post:
      tags: [Henvendelser]
      operationId: requestViewing
      summary: Be om visning
      description: Kun tilgjengelig når `booking.enabled` er true. Ellers 409 og nettsiden viser «Meld interesse».
      parameters:
        - { $ref: '#/components/parameters/Slug' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BookingRequest' }
      responses:
        '202': { $ref: '#/components/responses/Accepted' }
        '409': { $ref: '#/components/responses/Disabled' }
        '422': { $ref: '#/components/responses/Invalid' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
  /v1/integration/units/{slug}:
    put:
      tags: [Integrasjon]
      operationId: upsertUnit
      summary: Opprett eller oppdater bolig fra eiendomsdrift
      security:
        - oauth2: [units:write]
      parameters:
        - { $ref: '#/components/parameters/Slug' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
        - { $ref: '#/components/parameters/Signature' }
        - { $ref: '#/components/parameters/Timestamp' }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UnitWrite' }
      responses:
        '200':
          description: Oppdatert
          content: { application/json: { schema: { $ref: '#/components/schemas/Unit' } } }
        '201':
          description: Opprettet
          content: { application/json: { schema: { $ref: '#/components/schemas/Unit' } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/Invalid' }
  /v1/integration/units/{slug}/flags:
    patch:
      tags: [Integrasjon]
      operationId: setUnitFlags
      summary: Slå visning, interesse og prospekt av eller på
      security:
        - oauth2: [units:write]
      parameters:
        - { $ref: '#/components/parameters/Slug' }
        - { $ref: '#/components/parameters/IdempotencyKey' }
        - { $ref: '#/components/parameters/Signature' }
        - { $ref: '#/components/parameters/Timestamp' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                status: { $ref: '#/components/schemas/Status' }
                bookingEnabled: { type: boolean }
                interestEnabled: { type: boolean }
                prospectusAvailable: { type: boolean }
      responses:
        '200':
          description: Oppdatert
          content: { application/json: { schema: { $ref: '#/components/schemas/Unit' } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
webhooks:
  leadCreated:
    post:
      summary: Ny henvendelse sendt til eiendomsdrift
      description: |
        EGB sender henvendelser til eiendomsdriftssystemets registrerte URL.
        Signert med HMAC-SHA256 i `X-EGB-Signature` og `X-EGB-Timestamp`.
        Mottaker svarer 2xx innen 10 sekunder; ellers nye forsøk med eksponentiell backoff i 24 timer.
        Personopplysninger sendes kun hit, aldri til offentlige endepunkter.
      parameters:
        - { $ref: '#/components/parameters/Signature' }
        - { $ref: '#/components/parameters/Timestamp' }
      requestBody:
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LeadEvent' }
      responses:
        '200': { description: Mottatt }
components:
  securitySchemes:
    oauth2:
      type: oauth2
      description: Client credentials. Kun for eiendomsdriftssystemet. Kombineres alltid med HMAC-signatur.
      flows:
        clientCredentials:
          tokenUrl: https://auth.egb.no/oauth2/token
          scopes:
            units:write: Opprette og oppdatere boliger og flagg
  parameters:
    Slug:
      name: slug
      in: path
      required: true
      schema: { type: string, pattern: '^[a-z0-9-]{2,80}$' }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema: { type: string, format: uuid }
    Signature:
      name: X-EGB-Signature
      in: header
      required: true
      description: '`sha256=` + hex(HMAC-SHA256(secret, timestamp + "." + rå body))'
      schema: { type: string, pattern: '^sha256=[a-f0-9]{64}$' }
    Timestamp:
      name: X-EGB-Timestamp
      in: header
      required: true
      description: Unix-sekunder. Avvises ved mer enn 300 sekunders avvik.
      schema: { type: integer }
  responses:
    Accepted:
      description: Mottatt. Ingen personopplysninger returneres.
      content:
        application/json:
          schema:
            type: object
            properties:
              receiptId: { type: string }
              message: { type: string, example: 'Takk! Vi tar kontakt innen én virkedag.' }
    NotFound:
      description: Ikke funnet
      content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
    Disabled:
      description: Funksjonen er slått av for denne boligen
      content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
    Invalid:
      description: Valideringsfeil
      content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
    Unauthorized:
      description: Mangler eller ugyldig token
      content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
    Forbidden:
      description: Ugyldig signatur, utløpt tidsstempel eller manglende scope
      content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
    TooManyRequests:
      description: For mange forespørsler
      headers:
        Retry-After: { schema: { type: integer } }
      content: { application/problem+json: { schema: { $ref: '#/components/schemas/Problem' } } }
  schemas:
    Status:
      type: string
      enum: [til-leie, utleid, til-salgs, kommer, solgt]
    UnitList:
      type: object
      required: [apiVersion, units]
      properties:
        apiVersion: { type: string }
        generatedAt: { type: string, format: date-time }
        nextCursor: { type: [string, 'null'] }
        units:
          type: array
          items: { $ref: '#/components/schemas/Unit' }
    Unit:
      type: object
      required: [id, slug, status, listing, type, title, address, booking, interest, prospectus, updatedAt]
      properties:
        id: { type: string }
        slug: { type: string }
        status: { $ref: '#/components/schemas/Status' }
        listing: { type: string, enum: [leie, salg] }
        type: { type: string, enum: [enebolig, leilighet, rekkehus, prosjekt, naering] }
        title: { type: string }
        tagline: { type: string }
        description: { type: string }
        owner:
          type: [object, 'null']
          description: Juridisk eier/selger. Aldri leietaker eller privatpersoner.
          properties:
            name: { type: string }
            orgNr: { type: [string, 'null'], pattern: '^[0-9]{9}$' }
            role: { type: string, enum: [selger, utleier] }
        address:
          type: object
          properties:
            street: { type: string }
            postalCode: { type: [string, 'null'] }
            city: { type: string }
            municipality: { type: string }
            district: { type: string }
            cadastre: { type: string }
        facts: { type: object, additionalProperties: true }
        price:
          type: [object, 'null']
          properties:
            currency: { type: string, const: NOK }
            fixed: { type: integer }
            totalInclCosts: { type: integer }
            monthlyRent: { type: integer }
            indexRegulated: { type: boolean }
            binding: { type: boolean }
        highlights: { type: array, items: { type: string } }
        images:
          type: array
          items:
            type: object
            required: [src, alt]
            properties:
              src: { type: string }
              alt: { type: string }
              kind: { type: string, enum: [foto, illustrasjon, plantegning] }
        booking:
          type: object
          required: [enabled]
          properties:
            enabled: { type: boolean, description: 'true = «Book visning». false = nettsiden viser «Meld interesse».' }
            label: { type: string }
        interest:
          type: object
          required: [enabled]
          properties:
            enabled: { type: boolean }
            label: { type: string }
        prospectus:
          type: object
          required: [available]
          properties:
            available: { type: boolean }
            url: { type: [string, 'null'] }
            label: { type: string }
        salesKit:
          type: object
          description: Fullstendig salgssett. Sendes på e-post til den som ber om det, publiseres ikke åpent.
          required: [available]
          properties:
            available: { type: boolean }
            label: { type: string }
            delivery: { type: string, enum: [e-post] }
            documents: { type: array, items: { type: string } }
        sources: { type: array, items: { type: string } }
        updatedAt: { type: string, format: date-time }
    UnitWrite:
      description: Samme felt som Unit, unntatt id og updatedAt (settes av serveren).
      allOf:
        - $ref: '#/components/schemas/Unit'
    Contact:
      type: object
      required: [name, email, consent]
      properties:
        name: { type: string, minLength: 2, maxLength: 120 }
        email: { type: string, format: email, maxLength: 200 }
        phone: { type: string, pattern: '^\+?[0-9 ]{8,15}$' }
        message: { type: string, maxLength: 2000 }
        consent:
          type: boolean
          const: true
          description: Samtykke til at EGB lagrer og bruker opplysningene for å besvare henvendelsen.
        captchaToken: { type: string, maxLength: 4000 }
    InterestRequest:
      unevaluatedProperties: false
      allOf:
        - $ref: '#/components/schemas/Contact'
        - type: object
          properties:
            wantsProspectus: { type: boolean }
            wantsUpdates: { type: boolean }
    BookingRequest:
      unevaluatedProperties: false
      allOf:
        - $ref: '#/components/schemas/Contact'
        - type: object
          required: [preferredSlots]
          properties:
            preferredSlots:
              type: array
              minItems: 1
              maxItems: 3
              items: { type: string, format: date-time }
    LeadEvent:
      type: object
      properties:
        eventId: { type: string }
        type: { type: string, enum: [interest, booking] }
        unitSlug: { type: string }
        createdAt: { type: string, format: date-time }
        contact: { $ref: '#/components/schemas/Contact' }
    Problem:
      type: object
      properties:
        type: { type: string }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
        instance: { type: string }
