openapi: 3.1.0
info:
  title: Graveinfo API
  version: '1'
  description: |
    Offentlig API for å søke opp ledningseiere, sende gravemeldinger og integrere gravemottak.

    **Base URL:** `https://graveinfo.no/api/v1`

    Denne filen er den maskinlesbare fasiten for API-et. Den vedlikeholdes sammen med
    rutene i kildekoden og publiseres på `https://graveinfo.no/openapi.yaml`.

    ## Autentisering

    Alle endepunkter unntatt `GET /` og `GET /organizations/search` krever en API-nøkkel:

    ```
    Authorization: Bearer gv_live_<64 hex-tegn>
    ```

    Nøkler opprettes under **API-nøkler** i [dashbordet](https://graveinfo.no/dashboard/api-keys),
    av en administrator hos organisasjonen (ledningseier) eller entreprenørprofilen. Nøkkelen
    tilhører den organisasjonen eller entreprenøren som opprettet den, og alle kall skjer på
    dens vegne. Nøkkelen vises kun én gang ved opprettelse.

    En entreprenør som også er registrert som ledningseier (samme organisasjonsnummer) kan
    bruke én og samme nøkkel mot både gravemelding- og innboks-endepunktene.

    ## Scopes

    Nøkler tildeles scopes ved opprettelse. Søk og kommunedekning krever ikke scope.

    | Scope | Endepunkter |
    |-------|-------------|
    | `gravemeldinger:write` | `POST /gravemeldinger`, `POST /gravemeldinger/{id}/send` |
    | `gravemeldinger:read` | `GET /gravemeldinger`, `GET /gravemeldinger/{id}` |
    | `innboks:read` | `GET /innboks` |
    | `innboks:write` | `POST /innboks/{recipientId}/svar` |

    Manglende scope gir HTTP 403.

    ## Rate limiting

    Grensene er per API-nøkkel og per minutt (fast vindu). Ved overskridelse svarer API-et
    med HTTP 429. Det sendes ingen `X-RateLimit-*`-headere.

    | Endepunkt | Kall/minutt |
    |-----------|-------------|
    | `GET /organizations/search` uten nøkkel | 20 (per IP) |
    | `GET /organizations/search` med nøkkel | 100 |
    | `GET /coverage` | ingen grense |
    | `GET /gravemeldinger` | 60 |
    | `POST /gravemeldinger` | 10 |
    | `GET /gravemeldinger/{id}` | ingen grense |
    | `POST /gravemeldinger/{id}/send` | 5 |
    | `GET /innboks` | 60 |
    | `POST /innboks/{recipientId}/svar` | 30 |

    ## Feilformat

    ```json
    { "error": { "code": "VALIDATION_ERROR", "message": "...", "fields": { "plannedStart": "Må være i fremtiden" } } }
    ```

    `code` er en maskinlesbar SCREAMING_SNAKE_CASE-streng. `field` (søk) eller `fields`
    (gravemeldinger) følger med ved valideringsfeil. Se hvert endepunkt for gyldige koder.
    Merk at 401- og 429-kodene varierer mellom endepunktgruppene (`INVALID_API_KEY` /
    `UNAUTHORIZED`, `RATE_LIMIT_EXCEEDED` / `RATE_LIMIT`); sjekk HTTP-statusen, ikke koden,
    for disse to tilfellene.

    ## Koordinater

    Alle polygoner i API-et er GeoJSON i **EPSG:4326 (WGS84)** med `[lengdegrad, breddegrad]`.
    Graveinfo konverterer selv til og fra sitt interne UTM-system.

servers:
  - url: https://graveinfo.no/api/v1
    description: Produksjon

security:
  - ApiKeyAuth: []

tags:
  - name: Organisasjoner
    description: Søk og oppslag på ledningseiere og entreprenører
  - name: Kommunedekning
    description: Hvem dekker hvilke kommuner med hvilken infrastruktur
  - name: Gravemeldinger
    description: Opprett, send og følg opp gravemeldinger (for entreprenører)
  - name: Gravemottak
    description: Motta og svar på gravemeldinger (for ledningseiere)

components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      description: >
        API-nøkkel på formatet `gv_live_` + 64 hex-tegn (totalt 72 tegn).
        Opprettes i dashbordet og sendes i `Authorization: Bearer`-headeren.

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Maskinlesbar feilkode
              example: INVALID_PARAMETER
            message:
              type: string
              example: 'limit must be between 1 and 100'
            field:
              type: string
              description: Feltet som forårsaket feilen (kun `GET /organizations/search`)
              example: limit
            fields:
              type: object
              additionalProperties:
                type: string
              description: Feilmelding per felt (kun `VALIDATION_ERROR` fra `POST /gravemeldinger`; `/send` gir bare `message`)
              example:
                plannedStart: Må være i fremtiden

    Pagination:
      type: object
      required: [total, limit, offset]
      properties:
        total:
          type: integer
          description: Totalt antall treff, uavhengig av `limit`
        limit:
          type: integer
        offset:
          type: integer

    GeoJSONPolygon:
      type: object
      required: [type, coordinates]
      properties:
        type:
          type: string
          enum: [Polygon]
        coordinates:
          type: array
          description: Lukket ring i EPSG:4326, `[lengdegrad, breddegrad]`. Første og siste punkt er like.
          items:
            type: array
            items:
              type: array
              minItems: 2
              maxItems: 2
              items:
                type: number
          example:
            - - [10.7522, 59.9139]
              - [10.7535, 59.9139]
              - [10.7535, 59.915]
              - [10.7522, 59.915]
              - [10.7522, 59.9139]

    WorkType:
      type: string
      description: Arbeidstype på gravemeldingen
      enum:
        - telekommunikasjon
        - vann_avlop
        - hoyspenning
        - lavspenning
        - veiarbeid
        - grunnarbeid
        - fjernvarme
        - annet
        - pressing
        - boring

    Gravemottak:
      type: string
      enum: [api, email, none]
      description: |
        Hvordan organisasjonen mottar gravemeldinger.
        - `api`: Graveinfo håndterer mottaket (organisasjonen svarer i dashbordet eller via API)
        - `email`: organisasjonen mottar melding på e-post, eller via et eksternt skjema
          som entreprenøren må bruke i tillegg
        - `none`: ikke registrert

    Organization:
      type: object
      required: [id, name, orgnr, type, claimed]
      properties:
        id:
          type: string
          format: uuid
          description: Graveinfos interne ID for organisasjonen eller entreprenøren
        name:
          type: string
          example: Elvia AS
        orgnr:
          type: string
          pattern: '^\d{9}$'
          example: '920415523'
        type:
          type: string
          enum: [owner, contractor]
          description: '`owner` = ledningseier, `contractor` = entreprenør'
        address:
          type: [string, 'null']
          example: Postboks 853, 0104 Oslo
        email:
          type: [string, 'null']
          format: email
        phone:
          type: [string, 'null']
        website:
          type: [string, 'null']
          format: uri
        gravemottak:
          oneOf:
            - $ref: '#/components/schemas/Gravemottak'
            - type: 'null'
          description: '`null` for entreprenører'
        claimed:
          type: boolean
          description: Om organisasjonen har bekreftet sin profil i Graveinfo

    UtilityOwner:
      type: object
      required: [id, name, orgnr, infrastructureTypes, gravemottak, email]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          example: Oslo kommune VAV
        orgnr:
          type: string
          example: '991765887'
        infrastructureTypes:
          type: array
          items:
            type: string
          description: Infrastrukturtyper organisasjonen har registrert i kommunen, f.eks. `vann_avlop`, `lavspenning`
          example: [vann_avlop]
        gravemottak:
          $ref: '#/components/schemas/Gravemottak'
        email:
          type: [string, 'null']
          format: email
          description: E-postadresse for gravemeldinger

    GravemeldingStatus:
      type: string
      enum: [draft, sent, in_progress, completed]
      description: |
        - `draft`: opprettet, ikke sendt
        - `sent`: sendt, venter på svar
        - `in_progress`: alle har svart, eller saken venter på kommunen
        - `completed`: gravemeldingen er klar eller arkivert

    RecipientSummary:
      type: object
      required: [total, responded, pending, hasConflicts]
      properties:
        total:
          type: integer
        responded:
          type: integer
        pending:
          type: integer
        hasConflicts:
          type: boolean
          description: Minst én mottaker har svart `conflict`

    GravemeldingListItem:
      type: object
      required:
        [id, referenceNumber, status, title, plannedStart, plannedEnd, createdAt, sentAt, summary]
      properties:
        id:
          type: string
          format: uuid
        referenceNumber:
          type: [string, 'null']
          example: GI-2026-00042
        status:
          $ref: '#/components/schemas/GravemeldingStatus'
        title:
          type: [string, 'null']
          description: Alltid satt for gravemeldinger opprettet via API-et; kan være null for utkast laget i dashbordet
        plannedStart:
          type: string
          format: date
        plannedEnd:
          type: [string, 'null']
          format: date
        createdAt:
          type: [string, 'null']
          format: date-time
        sentAt:
          type: [string, 'null']
          format: date-time
        summary:
          $ref: '#/components/schemas/RecipientSummary'

    Recipient:
      type: object
      required:
        [
          organizationId,
          name,
          status,
          response,
          responseNote,
          respondedAt,
          hasKartblad,
          kartbladUrl,
        ]
      properties:
        organizationId:
          type: string
          format: uuid
        name:
          type: [string, 'null']
          example: Elvia AS
        status:
          type: string
          enum: [pending, responded]
        response:
          type: [string, 'null']
          enum: [no_conflict, conflict, needs_meeting, null]
          description: |
            - `no_conflict`: ingen infrastruktur i området
            - `conflict`: mottakeren har infrastruktur i området, ta kontakt
            - `needs_meeting`: mottakeren trenger befaring eller mer informasjon
        responseNote:
          type: [string, 'null']
        respondedAt:
          type: [string, 'null']
          format: date-time
        hasKartblad:
          type: boolean
          description: Om mottakeren har lastet opp kartblad
        kartbladUrl:
          type: [string, 'null']
          format: uri
          description: Signert nedlastingslenke til kartbladet, gyldig i én time. Hent detaljen på nytt for en ny lenke.

    Gravemelding:
      type: object
      required:
        [
          id,
          referenceNumber,
          status,
          title,
          location,
          plannedStart,
          plannedEnd,
          createdAt,
          sentAt,
          recipients,
          summary,
        ]
      properties:
        id:
          type: string
          format: uuid
        referenceNumber:
          type: [string, 'null']
          example: GI-2026-00042
        status:
          $ref: '#/components/schemas/GravemeldingStatus'
        title:
          type: [string, 'null']
          description: Alltid satt for gravemeldinger opprettet via API-et; kan være null for utkast laget i dashbordet
        location:
          oneOf:
            - $ref: '#/components/schemas/GeoJSONPolygon'
            - type: 'null'
        plannedStart:
          type: string
          format: date
        plannedEnd:
          type: [string, 'null']
          format: date
        createdAt:
          type: [string, 'null']
          format: date-time
        sentAt:
          type: [string, 'null']
          format: date-time
        recipients:
          type: array
          items:
            $ref: '#/components/schemas/Recipient'
        summary:
          $ref: '#/components/schemas/RecipientSummary'

    CreatedGravemelding:
      type: object
      description: Svar fra `POST /gravemeldinger` uten `autoSend`
      required: [id, referenceNumber, status, recipients, createdAt, _links]
      properties:
        id:
          type: string
          format: uuid
        referenceNumber:
          type: [string, 'null']
        status:
          type: string
          enum: [draft]
        recipients:
          type: array
          description: Ledningseiere Graveinfo fant for polygonet. Kan være tom; da feiler `/send` med `NO_RECIPIENTS_FOUND`.
          items:
            type: object
            required: [organizationId, name, status]
            properties:
              organizationId:
                type: string
                format: uuid
              name:
                type: string
              status:
                type: string
                enum: [pending]
        createdAt:
          type: string
          format: date-time
        _links:
          type: object
          required: [self, send]
          properties:
            self:
              type: string
              example: /api/v1/gravemeldinger/3fa85f64-5717-4562-b3fc-2c963f66afa6
            send:
              type: string
              example: /api/v1/gravemeldinger/3fa85f64-5717-4562-b3fc-2c963f66afa6/send
        _warnings:
          type: array
          description: 'Bare til stede når `autoSend: true` ble bedt om og sendingen feilet. Utkastet finnes; kall `/send` på nytt.'
          items:
            type: object
            required: [code, message]
            properties:
              code:
                type: string
                enum: [AUTO_SEND_FAILED]
              message:
                type: string
              detail:
                description: Feilkroppen fra `/send`, eller null hvis svaret ikke var JSON
                oneOf:
                  - $ref: '#/components/schemas/Error'
                  - type: 'null'

    SentGravemelding:
      type: object
      description: 'Svar fra `POST /gravemeldinger/{id}/send`, og fra `POST /gravemeldinger` med `autoSend: true`'
      required: [id, referenceNumber, status, sentAt, recipients, emailResults, _links]
      properties:
        id:
          type: string
          format: uuid
        referenceNumber:
          type: [string, 'null']
        status:
          type: string
          enum: [sent]
        sentAt:
          type: string
          format: date-time
        recipients:
          type: array
          items:
            type: object
            required: [organizationId, name, status]
            properties:
              organizationId:
                type: string
                format: uuid
              name:
                type: string
              status:
                type: string
                enum: [pending, responded]
                description: '`pending` rett etter sending; `responded` for mottakere Graveinfo besvarer automatisk under sendingen.'
        emailResults:
          type: array
          description: Resultat per e-postvarsel. Mottakere med gravemottak via eksternt skjema får ikke e-post og er ikke med her.
          items:
            type: object
            required: [recipientId, success]
            properties:
              recipientId:
                type: string
                format: uuid
              success:
                type: boolean
              error:
                type: string
        _links:
          type: object
          required: [self]
          properties:
            self:
              type: string

    InboxItem:
      type: object
      required: [recipientId, gravemelding, status, receivedAt]
      properties:
        recipientId:
          type: string
          format: uuid
          description: ID for mottakerrelasjonen. Brukes i `POST /innboks/{recipientId}/svar`.
        gravemelding:
          type: object
          required:
            [
              id,
              referenceNumber,
              title,
              plannedStart,
              plannedEnd,
              location,
              workTypes,
              description,
              contractor,
            ]
          properties:
            id:
              type: string
              format: uuid
            referenceNumber:
              type: [string, 'null']
            title:
              type: [string, 'null']
            plannedStart:
              type: [string, 'null']
              format: date
            plannedEnd:
              type: [string, 'null']
              format: date
            location:
              oneOf:
                - $ref: '#/components/schemas/GeoJSONPolygon'
                - type: 'null'
            workTypes:
              type: array
              items:
                $ref: '#/components/schemas/WorkType'
            description:
              type: [string, 'null']
            contractor:
              type: [object, 'null']
              required: [name, orgnr, contactPerson, phone, email]
              properties:
                name:
                  type: string
                orgnr:
                  type: string
                contactPerson:
                  type: [string, 'null']
                phone:
                  type: [string, 'null']
                email:
                  type: [string, 'null']
                  format: email
        status:
          type: string
          enum: [pending, responded]
        receivedAt:
          type: string
          format: date-time

  responses:
    ScopeMissing:
      description: Nøkkelen mangler scope, eller er ikke knyttet til riktig profil
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

paths:
  /:
    get:
      summary: API-rot
      operationId: getApiRoot
      description: Tjenesteoppdagelse. Returnerer navn, versjon, lenke til dokumentasjonen og listen over offentlige endepunkter. Krever ikke nøkkel.
      security:
        - {}
      responses:
        '200':
          description: API-metadata
          content:
            application/json:
              schema:
                type: object
                required: [name, version, documentation, endpoints]
                properties:
                  name:
                    type: string
                    example: Graveinfo API
                  version:
                    type: string
                    example: v1
                  documentation:
                    type: string
                    format: uri
                    example: https://graveinfo.no/for-utviklere/api-referanse
                  endpoints:
                    type: array
                    items:
                      type: object
                      required: [method, path, description]
                      properties:
                        method:
                          type: string
                        path:
                          type: string
                          example: /api/v1/gravemeldinger
                        description:
                          type: string

  /organizations/search:
    get:
      summary: Søk etter organisasjoner
      operationId: searchOrganizations
      tags: [Organisasjoner]
      description: |
        Søk etter ledningseiere og/eller entreprenører på navn eller organisasjonsnummer.
        Kan kalles uten API-nøkkel (20 kall/minutt per IP) eller med nøkkel (100 kall/minutt).
        Krever ikke scope.

        Uten `q` eller `orgnr` returneres en tom liste. Er begge satt, brukes `orgnr`.
      security:
        - ApiKeyAuth: []
        - {}
      parameters:
        - name: q
          in: query
          description: Fritekstsøk på navn eller organisasjonsnummer (delstreng)
          schema:
            type: string
          example: Elvia
        - name: orgnr
          in: query
          description: Eksakt oppslag på organisasjonsnummer
          schema:
            type: string
            pattern: '^\d{9}$'
        - name: type
          in: query
          schema:
            type: string
            enum: [owner, contractor, all]
            default: owner
        - name: municipality
          in: query
          description: Kommunenummer (4 siffer). Begrenser ledningseiere til de som er registrert i kommunen. Påvirker ikke entreprenører.
          schema:
            type: string
            pattern: '^\d{4}$'
          example: '0301'
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        '200':
          description: Søkeresultat
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Organization'
                  meta:
                    $ref: '#/components/schemas/Pagination'
              example:
                data:
                  - id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
                    name: Elvia AS
                    orgnr: '920415523'
                    type: owner
                    address: Postboks 853, 0104 Oslo
                    email: gravemelding@elvia.no
                    phone: null
                    website: https://www.elvia.no
                    gravemottak: api
                    claimed: true
                meta:
                  total: 1
                  limit: 20
                  offset: 0
        '400':
          description: Ugyldig parameter (`INVALID_PARAMETER`, med `field`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: INVALID_PARAMETER
                  message: 'type must be one of: owner, contractor, all'
                  field: type
        '401':
          description: Nøkkel oppgitt, men ugyldig (`INVALID_API_KEY`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit (`RATE_LIMIT_EXCEEDED`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /coverage:
    get:
      summary: Ledningseiere i en kommune
      operationId: getCoverage
      tags: [Kommunedekning]
      description: |
        Alle ledningseiere registrert i en kommune, med infrastrukturtyper og gravemottak.
        Krever nøkkel, men ikke scope. Ingen rate limit.
      parameters:
        - name: municipality
          in: query
          required: true
          description: Kommunenummer (4 siffer)
          schema:
            type: string
            pattern: '^\d{4}$'
          example: '0301'
        - name: type
          in: query
          description: Bare ledningseiere med denne infrastrukturtypen, f.eks. `vann_avlop`
          schema:
            type: string
      responses:
        '200':
          description: Kommunedekning
          content:
            application/json:
              schema:
                type: object
                required: [municipality, owners, total]
                properties:
                  municipality:
                    type: object
                    required: [number, name]
                    properties:
                      number:
                        type: string
                        example: '0301'
                      name:
                        type: string
                        example: Oslo
                  owners:
                    type: array
                    items:
                      $ref: '#/components/schemas/UtilityOwner'
                  total:
                    type: integer
              example:
                municipality:
                  number: '0301'
                  name: Oslo
                owners:
                  - id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
                    name: Oslo kommune VAV
                    orgnr: '991765887'
                    infrastructureTypes: [vann_avlop]
                    gravemottak: api
                    email: gravemelding@vav.oslo.kommune.no
                total: 1
        '400':
          description: '`MISSING_PARAMETER` eller `INVALID_PARAMETER`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Manglende eller ugyldig nøkkel (`UNAUTHORIZED`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Kommunenummeret finnes ikke (`MUNICIPALITY_NOT_FOUND`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: '`INTERNAL_ERROR`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /gravemeldinger:
    post:
      summary: Opprett en gravemelding
      operationId: createGravemelding
      tags: [Gravemeldinger]
      description: |
        Oppretter en gravemelding som **utkast** for entreprenøren nøkkelen tilhører, og finner
        ledningseierne som dekker polygonet. Kommunen utledes fra polygonet.

        Utkastet sendes med `POST /gravemeldinger/{id}/send`, eller umiddelbart med
        `autoSend: true`. Med `autoSend` returneres `/send`-svaret med HTTP 201; feiler
        sendingen, returneres utkastet med `_warnings`.

        Scope: `gravemeldinger:write`. Nøkkelen må tilhøre, eller dele organisasjonsnummer
        med, en registrert entreprenør; ellers `MISSING_CONTRACTOR`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, location, plannedStart, plannedEnd, workTypes, contactPerson]
              properties:
                title:
                  type: string
                  example: Graving Storgata 12
                location:
                  type: object
                  required: [coordinates]
                  properties:
                    type:
                      type: string
                      enum: [Polygon]
                      description: Valgfri; må være `Polygon` hvis satt
                    coordinates:
                      $ref: '#/components/schemas/GeoJSONPolygon/properties/coordinates'
                plannedStart:
                  type: string
                  format: date
                  description: Dagens dato eller senere
                  example: '2026-10-01'
                plannedEnd:
                  type: string
                  format: date
                  description: Samme dag som eller etter `plannedStart`
                  example: '2026-10-14'
                workTypes:
                  type: array
                  minItems: 1
                  items:
                    $ref: '#/components/schemas/WorkType'
                description:
                  type: string
                contactPerson:
                  type: object
                  required: [name, phone]
                  properties:
                    name:
                      type: string
                    phone:
                      type: string
                    email:
                      type: string
                      format: email
                      description: Tas imot, men lagres ikke i dag
                autoSend:
                  type: boolean
                  default: false
            example:
              title: Graving Storgata 12
              description: Legging av fiberkabel langs Storgata 12-18
              workTypes: [telekommunikasjon, grunnarbeid]
              location:
                type: Polygon
                coordinates:
                  - - [10.7522, 59.9139]
                    - [10.7535, 59.9139]
                    - [10.7535, 59.915]
                    - [10.7522, 59.915]
                    - [10.7522, 59.9139]
              plannedStart: '2026-10-01'
              plannedEnd: '2026-10-14'
              contactPerson:
                name: Ola Nordmann
                phone: '91234567'
              autoSend: false
      responses:
        '201':
          description: 'Utkast opprettet, eller sendt når `autoSend: true`'
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/CreatedGravemelding'
                  - $ref: '#/components/schemas/SentGravemelding'
              example:
                id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
                referenceNumber: GI-2026-00042
                status: draft
                recipients:
                  - organizationId: 7cb5ab12-8901-4de5-b3fc-1a234d56e78f
                    name: Elvia AS
                    status: pending
                  - organizationId: 9ab12cd3-4567-8ef9-a0bc-1d234e56f78a
                    name: Oslo kommune VAV
                    status: pending
                createdAt: '2026-09-15T10:00:00.000Z'
                _links:
                  self: /api/v1/gravemeldinger/3fa85f64-5717-4562-b3fc-2c963f66afa6
                  send: /api/v1/gravemeldinger/3fa85f64-5717-4562-b3fc-2c963f66afa6/send
        '400':
          description: |
            - `INVALID_JSON`: kroppen er ikke gyldig JSON
            - `VALIDATION_ERROR`: se `fields` (`title`, `location.coordinates`, `location.type`,
              `plannedStart`, `plannedEnd`, `workTypes`, `contactPerson.name`, `contactPerson.phone`)
            - `NO_MUNICIPALITIES_FOUND`: polygonet treffer ingen norsk kommune (typisk feil koordinatsystem)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error:
                  code: VALIDATION_ERROR
                  message: Validation failed
                  fields:
                    location.coordinates: Required
                    plannedStart: Required
        '401':
          description: '`INVALID_API_KEY`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: '`INSUFFICIENT_SCOPE` eller `MISSING_CONTRACTOR`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: '`RATE_LIMIT_EXCEEDED` (10 kall/minutt)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: '`SERVER_ERROR`: intern feil, prøv igjen'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

    get:
      summary: List egne gravemeldinger
      operationId: listGravemeldinger
      tags: [Gravemeldinger]
      description: |
        Gravemeldinger som tilhører entreprenøren nøkkelen er knyttet til, nyeste først.

        Scope: `gravemeldinger:read`
      parameters:
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/GravemeldingStatus'
        - name: from
          in: query
          description: Opprettet fra og med (ISO 8601-dato eller -tidspunkt)
          schema:
            type: string
            format: date
        - name: to
          in: query
          description: Opprettet til og med (ISO 8601-dato eller -tidspunkt)
          schema:
            type: string
            format: date
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        '200':
          description: Liste
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/GravemeldingListItem'
                  meta:
                    $ref: '#/components/schemas/Pagination'
        '400':
          description: '`INVALID_PARAMETER`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: '`INVALID_API_KEY`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: '`FORBIDDEN` (mangler scope) eller `MISSING_CONTRACTOR`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: '`RATE_LIMIT_EXCEEDED` (60 kall/minutt)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /gravemeldinger/{id}:
    get:
      summary: Hent en gravemelding
      operationId: getGravemelding
      tags: [Gravemeldinger]
      description: |
        Full status med svar fra hver mottaker. Gravemeldinger som tilhører andre gir 404.

        Scope: `gravemeldinger:read`
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Gravemelding
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Gravemelding'
              example:
                id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
                referenceNumber: GI-2026-00042
                status: sent
                title: Graving Storgata 12
                location:
                  type: Polygon
                  coordinates:
                    - - [10.7522, 59.9139]
                      - [10.7535, 59.9139]
                      - [10.7535, 59.915]
                      - [10.7522, 59.915]
                      - [10.7522, 59.9139]
                plannedStart: '2026-10-01'
                plannedEnd: '2026-10-14'
                createdAt: '2026-09-15T10:00:00.000Z'
                sentAt: '2026-09-15T10:00:01.000Z'
                recipients:
                  - organizationId: 7cb5ab12-8901-4de5-b3fc-1a234d56e78f
                    name: Elvia AS
                    status: responded
                    response: no_conflict
                    responseNote: Ingen kabler i dette området.
                    respondedAt: '2026-09-15T14:30:00.000Z'
                    hasKartblad: true
                    kartbladUrl: https://storage.graveinfo.no/kartblad/...?signature=...
                  - organizationId: 9ab12cd3-4567-8ef9-a0bc-1d234e56f78a
                    name: Oslo kommune VAV
                    status: pending
                    response: null
                    responseNote: null
                    respondedAt: null
                    hasKartblad: false
                    kartbladUrl: null
                summary:
                  total: 2
                  responded: 1
                  pending: 1
                  hasConflicts: false
        '401':
          description: '`INVALID_API_KEY`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: '`FORBIDDEN` (mangler scope)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Finnes ikke, eller tilhører en annen entreprenør (`NOT_FOUND`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /gravemeldinger/{id}/send:
    post:
      summary: Send en gravemelding
      operationId: sendGravemelding
      tags: [Gravemeldinger]
      description: |
        Sender et utkast til mottakerne: varsler ledningseierne, setter frister og endrer
        status fra `draft` til `sent`. Ingen kropp. Kan bare kalles én gang per gravemelding.

        Scope: `gravemeldinger:write`
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Sendt
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SentGravemelding'
              example:
                id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
                referenceNumber: GI-2026-00042
                status: sent
                sentAt: '2026-09-15T10:00:01.000Z'
                recipients:
                  - organizationId: 7cb5ab12-8901-4de5-b3fc-1a234d56e78f
                    name: Elvia AS
                    status: pending
                emailResults:
                  - recipientId: a1b2c3d4-5678-90ab-cdef-1234567890ab
                    success: true
                _links:
                  self: /api/v1/gravemeldinger/3fa85f64-5717-4562-b3fc-2c963f66afa6
        '400':
          description: |
            - `VALIDATION_ERROR`: utkastet mangler påkrevde felt
            - `NO_RECIPIENTS_FOUND`: ingen ledningseiere i området
            - `INVALID_PLAN`: gravemeldingen har kostnadsfri plan og kan ikke sendes via API
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: '`INVALID_API_KEY`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: '`INSUFFICIENT_SCOPE`, `MISSING_CONTRACTOR`, `CONTRACTOR_NOT_FOUND` eller `FORBIDDEN` (tilhører en annen entreprenør)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`NOT_FOUND`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: '`INVALID_STATUS`: gravemeldingen er allerede sendt (eller er ikke et utkast). `PRACTICE_NOT_SENDABLE`: onboarding-øvelse, kan ikke sendes.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: '`RATE_LIMIT_EXCEEDED` (5 kall/minutt)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: '`SERVER_ERROR`: intern feil, prøv igjen'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /innboks:
    get:
      summary: Hent innboks
      operationId: getInnboks
      tags: [Gravemottak]
      description: |
        For ledningseiere: gravemeldinger der organisasjonen nøkkelen tilhører er mottaker,
        nyeste først. Utkast og gravemeldinger med kostnadsfri plan vises ikke.

        Bruk `recipientId` til å svare med `POST /innboks/{recipientId}/svar`.

        Scope: `innboks:read`. Nøkkelen må være knyttet til en organisasjon; ellers `MISSING_ORGANIZATION`.
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, responded, all]
            default: all
        - name: limit
          in: query
          description: Verdier utenfor 1-100 klippes til grensen
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        '200':
          description: Innboks
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/InboxItem'
                  meta:
                    $ref: '#/components/schemas/Pagination'
              example:
                data:
                  - recipientId: a1b2c3d4-5678-90ab-cdef-1234567890ab
                    gravemelding:
                      id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
                      referenceNumber: GI-2026-00042
                      title: Graving Storgata 12
                      plannedStart: '2026-10-01'
                      plannedEnd: '2026-10-14'
                      location:
                        type: Polygon
                        coordinates:
                          - - [10.7522, 59.9139]
                            - [10.7535, 59.9139]
                            - [10.7535, 59.915]
                            - [10.7522, 59.915]
                            - [10.7522, 59.9139]
                      workTypes: [telekommunikasjon, grunnarbeid]
                      description: Legging av fiberkabel langs Storgata 12-18
                      contractor:
                        name: Graving AS
                        orgnr: '987654321'
                        contactPerson: Ola Nordmann
                        phone: '91234567'
                        email: post@graving.no
                    status: pending
                    receivedAt: '2026-09-15T10:00:01.000Z'
                meta:
                  total: 1
                  limit: 20
                  offset: 0
        '400':
          description: '`BAD_REQUEST` (ugyldig `status`)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: '`UNAUTHORIZED`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: '`FORBIDDEN` (mangler scope) eller `MISSING_ORGANIZATION`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: '`RATE_LIMIT` (60 kall/minutt), med `Retry-After: 60`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /innboks/{recipientId}/svar:
    post:
      summary: Svar på en gravemelding
      operationId: respondToGravemelding
      tags: [Gravemottak]
      description: |
        For ledningseiere: registrerer svar på en mottatt gravemelding. Et nytt kall på samme
        `recipientId` overskriver forrige svar og varsler entreprenøren på nytt hvis `note` er satt.

        Scope: `innboks:write`

        Intern, plattform-only unntaksvei (landmarker-platform-design ADR 079 D3): driftsstøtte-
        modulen svarer på vegne av en tilkoblet eier over Zitadel M2M i stedet for en kunde-API-
        nøkkel. Da er `organization_id` påkrevd i body, og kallet avvises med 403 hvis
        organisasjonen ikke faktisk er koblet til driftsstøtte. Ikke tilgjengelig for kunder -
        M2M-legitimasjonen utstedes kun til driftsstøtte selv.
      parameters:
        - name: recipientId
          in: path
          required: true
          description: '`recipientId` fra `GET /innboks`'
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [response]
              properties:
                response:
                  type: string
                  enum: [no_conflict, conflict, needs_meeting]
                  description: |
                    - `no_conflict`: ingen infrastruktur i graveområdet
                    - `conflict`: vi har infrastruktur i området, ta kontakt
                    - `needs_meeting`: trenger befaring eller mer informasjon
                note:
                  type: string
                  description: Fritekst til entreprenøren
                attachmentUrl:
                  type: [string, 'null']
                  format: uri
                  description: Tas imot og logges, men vises ikke for entreprenøren i dag. Legg lenken i `note` hvis entreprenøren skal se den.
                acting_user_id:
                  type: string
                  description: |
                    Valgfri. Identifiserer hvilken saksbehandler som svarte, når kallet gjøres
                    på organisasjonens vegne av et annet system. Fritekst, ikke verifisert av
                    graveinfo - logges kun til revisjonssporet.
                organization_id:
                  type: string
                  format: uuid
                  description: |
                    Påkrevd kun for det interne M2M-unntaket over (driftsstøtte). Ugyldig/utelatt
                    for kunde-API-nøkkel-kall - autentiseringen bærer allerede organisasjonen.
            example:
              response: no_conflict
              note: Ingen ledninger i dette området.
      responses:
        '200':
          description: Svar registrert
          content:
            application/json:
              schema:
                type: object
                required: [recipientId, status, respondedAt]
                properties:
                  recipientId:
                    type: string
                    format: uuid
                  status:
                    type: string
                    enum: [responded]
                  respondedAt:
                    type: string
                    format: date-time
        '400':
          description: '`BAD_REQUEST` (ugyldig JSON eller ugyldig `response`)'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: '`UNAUTHORIZED`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: '`FORBIDDEN`: mangler scope, eller mottakeren tilhører en annen organisasjon'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`NOT_FOUND`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: '`LOCKED`: organisasjonen svarer på gravemeldinger i driftsstøtte, ikke i graveinfo'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: '`RATE_LIMIT` (30 kall/minutt), med `Retry-After: 60`'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: '`INTERNAL_ERROR`: intern feil, prøv igjen'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
