openapi: 3.0.3
info:
  title: Velvari API
  version: "0.8.3-beta"
  description: |
    Canonical OpenAPI contract for `velvari-api`.

    This file, `docs/openapi.yaml`, is the source of truth for external API documentation and for the Astro docs pipeline.
    The same document is also served by the runtime at `GET /openapi.yaml`.
    Public meta endpoints are GET-only: `/`, `/healthz`, and `/openapi.yaml`.

    API contract versioning remains path-based. The active taxi-card contract is `/v2/cards/taxi`; `/v1/ors/*` and `/v1/suno/*` remain separate namespaced APIs.
    OpenAPI `info.version` is the spec/release version and does not replace route versions.

    Canonical taxi-card paths use the strict v2 contract at `/v2/cards/taxi`.
    Canonical ORS proxy paths are `/v1/ors/directions/{profile}` and `/v1/ors/matrix/{profile}`.
    Bare exact routes without `{profile}` exist only so runtime can reject missing profiles and are intentionally excluded from the canonical contract.
x-release-date: "2026-05-04"
x-api-stage: "beta"
servers:
  - url: https://api.velvari.ee
    description: Production runtime and raw OpenAPI delivery endpoint
  - url: http://localhost:8080
    description: Local development server
tags:
  - name: Meta
    description: Public metadata, health, and raw contract delivery endpoints.
  - name: Taxi Cards v2
    description: Strict machine-friendly taxi-card contract for new integrations.
  - name: OpenRouteService
    description: OpenRouteService proxy and normalized helper endpoints under `/v1/ors/*`.
  - name: Suno
    description: Authenticated cached public Suno profile song index endpoints.
security:
  - ApiKeyHeaderAuth: []
  - BearerApiKeyAuth: []
paths:
  /:
    get:
      tags: [Meta]
      operationId: getRuntimeIndex
      security: []
      summary: Runtime index
      description: Lightweight public text endpoint that identifies the runtime host.
      responses:
        "200":
          description: Plain-text runtime identifier
          content:
            text/plain:
              schema:
                type: string
                example: api.velvari.ee

  /healthz:
    get:
      tags: [Meta]
      operationId: getHealthz
      security: []
      summary: Health check
      description: Public liveness endpoint used by deployment and health-check tooling.
      responses:
        "200":
          description: Plain-text health response
          content:
            text/plain:
              schema:
                type: string
                example: ok

  /openapi.yaml:
    get:
      tags: [Meta]
      operationId: getOpenAPIYAML
      security: []
      summary: Raw OpenAPI document
      description: Returns the canonical OpenAPI specification from `docs/openapi.yaml` for humans and tooling.
      responses:
        "200":
          description: Canonical OpenAPI YAML document
          content:
            application/yaml:
              schema:
                type: string
            text/yaml:
              schema:
                type: string

  /v1/suno/profiles/{handle}/songs:
    get:
      tags: [Suno]
      operationId: listSunoProfileSongs
      summary: Read cached public Suno profile songs
      description: >
        Reads the local JSON snapshot for an allowed public Suno profile handle and returns the normalized Songs-section snapshot.
        This endpoint does not fetch Suno live. Snapshot data comes from public Suno profile metadata and anonymous public unified feed pagination.
      parameters:
        - name: handle
          in: path
          required: true
          description: Public Suno profile handle without a leading `@`. Must be enabled by `SUNO_ALLOWED_HANDLES`.
          schema:
            type: string
            pattern: "^[A-Za-z0-9._-]{1,64}$"
            example: akaki
      responses:
        "200":
          description: Cached Suno profile song index
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SunoProfileSongsResponse" }
              examples:
                cachedProfileSongs:
                  $ref: "#/components/examples/SunoProfileSongsResponseExample"
        "400":
          description: Invalid handle format
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "401":
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "403":
          description: Handle is not allowed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "404":
          description: Snapshot not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "429":
          description: Rate limited
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "500":
          description: Server error
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /v1/suno/songs/search:
    get:
      tags: [Suno]
      operationId: searchSunoSongs
      summary: Search cached Suno songs by artist and title
      description: >
        Reads the cached snapshot for an allowed Suno profile handle passed as `artist` and returns matching normalized songs by title.
        This endpoint does not call Suno live; it exists so clients can request full track metadata, counters, lyrics, and style fields without downloading the whole profile song list.
        Exact title matching is used by default. Set `exact=false` for case-insensitive substring matching.
      parameters:
        - name: artist
          in: query
          required: true
          description: Public Suno profile handle without a leading `@`. Must be enabled by `SUNO_ALLOWED_HANDLES`.
          schema:
            type: string
            pattern: "^[A-Za-z0-9._-]{1,64}$"
            example: akaki
        - name: title
          in: query
          required: true
          description: Song title to match against cached Suno song titles.
          schema:
            type: string
            minLength: 1
            example: Night Ride
        - name: exact
          in: query
          required: false
          description: Set to `false` to use case-insensitive substring title matching.
          schema:
            type: boolean
            default: true
      responses:
        "200":
          description: Matching cached Suno songs with full normalized metadata
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SunoSongSearchResponse" }
              examples:
                cachedSongSearch:
                  $ref: "#/components/examples/SunoSongSearchResponseExample"
        "400":
          description: Invalid or missing query
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "401":
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "403":
          description: Artist handle is not allowed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "404":
          description: Snapshot or song not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "429":
          description: Rate limited
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "500":
          description: Server error
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /v1/suno/songs/{id}:
    get:
      tags: [Suno]
      operationId: getSunoSongByID
      summary: Read a cached Suno song by ID
      description: >
        Searches allowed cached Suno profile snapshots for a public song UUID and returns the full normalized song item.
        This endpoint does not call Suno live.
      parameters:
        - name: id
          in: path
          required: true
          description: Public Suno song UUID.
          schema:
            type: string
            format: uuid
            example: 132ec409-86b3-4587-87f8-a32ae30446d3
      responses:
        "200":
          description: Cached Suno song
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SunoSongLookupResponse" }
              examples:
                cachedSong:
                  $ref: "#/components/examples/SunoSongLookupResponseExample"
        "400":
          description: Invalid song ID
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "401":
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "404":
          description: Song not found in allowed cached snapshots
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "429":
          description: Rate limited
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "500":
          description: Server error
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /v1/suno/profiles/{handle}/songs/search:
    get:
      tags: [Suno]
      operationId: searchSunoProfileSongs
      summary: Search and filter cached Suno profile songs
      description: >
        Searches one allowed cached Suno profile snapshot by title, style, lyrics, model, and minimum play count.
        Results can be sorted and limited without downloading the full profile snapshot.
      parameters:
        - name: handle
          in: path
          required: true
          description: Public Suno profile handle without a leading `@`. Must be enabled by `SUNO_ALLOWED_HANDLES`.
          schema:
            type: string
            pattern: "^[A-Za-z0-9._-]{1,64}$"
            example: akaki
        - name: title
          in: query
          required: false
          schema: { type: string, example: Night Ride }
        - name: style
          in: query
          required: false
          schema: { type: string, example: electro-pop }
        - name: lyrics
          in: query
          required: false
          schema: { type: string, example: neon }
        - name: model
          in: query
          required: false
          schema: { type: string, example: v5 }
        - name: min_play_count
          in: query
          required: false
          schema: { type: integer, minimum: 0, example: 10 }
        - name: sort
          in: query
          required: false
          description: Supported fields are `play_count`, `upvote_count`, `comment_count`, `duration_seconds`, `created_at`, and `title`; prefix with `-` for descending order.
          schema: { type: string, example: "-play_count" }
        - name: limit
          in: query
          required: false
          schema: { type: integer, minimum: 0, maximum: 200, default: 50, example: 10 }
        - name: exact
          in: query
          required: false
          description: Exact matching applies to title and model; style and lyrics use substring matching.
          schema: { type: boolean, default: true }
      responses:
        "200":
          description: Matching cached Suno songs with full normalized metadata
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SunoSongSearchResponse" }
              examples:
                cachedSongSearch:
                  $ref: "#/components/examples/SunoSongSearchResponseExample"
        "400":
          description: Invalid or missing query
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "401":
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "403":
          description: Handle is not allowed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "404":
          description: Snapshot or song not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "429":
          description: Rate limited
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "500":
          description: Server error
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /v1/suno/profiles/{handle}/stats:
    get:
      tags: [Suno]
      operationId: getSunoProfileStats
      summary: Read cached Suno profile stats
      description: >
        Aggregates counters, model/style buckets, duration, and top songs from the cached Suno profile snapshot.
        This endpoint does not call Suno live.
      parameters:
        - name: handle
          in: path
          required: true
          description: Public Suno profile handle without a leading `@`. Must be enabled by `SUNO_ALLOWED_HANDLES`.
          schema:
            type: string
            pattern: "^[A-Za-z0-9._-]{1,64}$"
            example: akaki
      responses:
        "200":
          description: Cached Suno profile stats
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SunoProfileStatsResponse" }
              examples:
                cachedProfileStats:
                  $ref: "#/components/examples/SunoProfileStatsResponseExample"
        "400":
          description: Invalid handle format
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "401":
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "403":
          description: Handle is not allowed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "404":
          description: Snapshot not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "429":
          description: Rate limited
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "500":
          description: Server error
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /v1/suno/profiles/{handle}/sync:
    post:
      tags: [Suno]
      operationId: syncSunoProfileSongs
      summary: Refresh cached public Suno profile songs
      description: >
        Reads allowed public Suno profile metadata, resolves the public target user ID, paginates anonymous public unified feed `user_songs`
        with a top-level cursor, extracts valid public song clips, then atomically writes a local JSON snapshot.
        The sync is bounded by configured page and timeout limits, deduplicates songs by ID, and prevents a warning-state partial
        result from overwriting a larger existing snapshot. No private Suno account token, Browser-Token, Device-Id, cookies, or login flow is required.
      parameters:
        - name: handle
          in: path
          required: true
          description: Public Suno profile handle without a leading `@`. Must be enabled by `SUNO_ALLOWED_HANDLES`.
          schema:
            type: string
            pattern: "^[A-Za-z0-9._-]{1,64}$"
            example: akaki
      responses:
        "200":
          description: Refreshed Suno profile song index
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SunoProfileSongsResponse" }
              examples:
                refreshedProfileSongs:
                  $ref: "#/components/examples/SunoProfileSongsResponseExample"
        "400":
          description: Invalid handle format
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "401":
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "403":
          description: Handle is not allowed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "429":
          description: Rate limited
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "500":
          description: Snapshot write or internal error
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "503":
          description: Suno public profile fetch, sync, or parser failure
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /v1/ors/route:
    get:
      tags: [OpenRouteService]
      operationId: getORSRoute
      summary: ORS route summary
      description: Returns a compact route summary with normalized geometry for map rendering.
      parameters:
        - name: start_lng
          in: query
          required: true
          schema: { type: number, format: double }
        - name: start_lat
          in: query
          required: true
          schema: { type: number, format: double }
        - name: end_lng
          in: query
          required: true
          schema: { type: number, format: double }
        - name: end_lat
          in: query
          required: true
          schema: { type: number, format: double }
        - name: profile
          in: query
          required: false
          schema:
            type: string
            default: driving-car
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ORSRouteSummary" }
              examples:
                routeSummary:
                  $ref: "#/components/examples/ORSRouteSummaryExample"
        "400":
          description: Invalid request
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "401":
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "404":
          description: Route not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "429":
          description: Rate limited
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "500":
          description: Server error
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "503":
          description: Upstream unavailable or ORS is not configured
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /v1/ors/autocomplete:
    get:
      tags: [OpenRouteService]
      operationId: getORSAutocomplete
      summary: ORS autocomplete
      description: Returns normalized address and place suggestions from OpenRouteService geocoding autocomplete.
      parameters:
        - name: text
          in: query
          required: true
          schema: { type: string }
        - name: size
          in: query
          schema:
            type: integer
            default: 5
            minimum: 1
            maximum: 20
        - name: focus_lat
          in: query
          schema: { type: number, format: double }
        - name: focus_lng
          in: query
          schema: { type: number, format: double }
        - name: country
          in: query
          schema: { type: string }
          description: Country code forwarded to ORS boundary filtering.
        - name: layers
          in: query
          schema:
            type: string
            example: venue,address
          description: Comma-separated ORS layers.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ORSAutocompleteResponse" }
              examples:
                autocompleteSuggestions:
                  $ref: "#/components/examples/ORSAutocompleteResponseExample"
        "400":
          description: Invalid request
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "401":
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "404":
          description: No suggestions found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "429":
          description: Rate limited
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "500":
          description: Server error
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "503":
          description: Upstream unavailable or ORS is not configured
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /v1/ors/reverse:
    get:
      tags: [OpenRouteService]
      operationId: getORSReverse
      summary: ORS reverse geocode
      description: Returns a human-readable address label for the given coordinates.
      parameters:
        - name: lat
          in: query
          required: true
          schema: { type: number, format: double }
        - name: lng
          in: query
          required: true
          schema: { type: number, format: double }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ORSReverseResponse" }
              examples:
                reverseGeocode:
                  $ref: "#/components/examples/ORSReverseResponseExample"
        "400":
          description: Invalid request
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "401":
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "404":
          description: Result not found
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "429":
          description: Rate limited
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "500":
          description: Server error
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "503":
          description: Upstream unavailable or ORS is not configured
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /v1/ors/isochrones:
    get:
      tags: [OpenRouteService]
      operationId: getORSIsochrones
      summary: ORS isochrones
      description: |
        Thin GET wrapper over OpenRouteService isochrones.

        Successful responses and upstream HTTP error payloads are passed through as JSON when the request reaches ORS.
        Local auth, rate-limit, and validation failures use the standard Velvari `ErrorResponse` envelope.
      parameters:
        - name: lng
          in: query
          required: true
          schema: { type: number, format: double }
        - name: lat
          in: query
          required: true
          schema: { type: number, format: double }
        - name: range
          in: query
          required: true
          schema:
            type: string
            example: 300,600
          description: Comma-separated integers forwarded to ORS `range`.
        - name: profile
          in: query
          schema:
            type: string
            default: driving-car
        - name: range_type
          in: query
          schema:
            type: string
            default: time
        - name: interval
          in: query
          schema:
            type: integer
      responses:
        "200":
          description: Upstream ORS GeoJSON payload
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ORSPassthroughJSON" }
              examples:
                isochronesGeoJSON:
                  $ref: "#/components/examples/ORSIsochronesResponseExample"
        "400":
          description: Invalid request or upstream ORS validation error payload
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/ErrorResponse" }
                  - { $ref: "#/components/schemas/ORSPassthroughJSON" }
        "401":
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "404":
          description: Upstream ORS not-found payload
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ORSPassthroughJSON" }
        "429":
          description: Rate limited locally or by upstream ORS
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/ErrorResponse" }
                  - { $ref: "#/components/schemas/ORSPassthroughJSON" }
        "500":
          description: Server error or upstream ORS error payload
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/ErrorResponse" }
                  - { $ref: "#/components/schemas/ORSPassthroughJSON" }
        "503":
          description: Upstream unavailable, ORS not configured, or upstream ORS error payload
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/ErrorResponse" }
                  - { $ref: "#/components/schemas/ORSPassthroughJSON" }

  /v1/ors/directions/{profile}:
    post:
      tags: [OpenRouteService]
      operationId: postORSDirections
      summary: ORS directions proxy
      description: |
        Thin proxy to OpenRouteService directions v2.

        The request body and successful response payloads are passed through as JSON.
        If ORS returns an HTTP error response, that upstream JSON payload is also passed through with the upstream status code.
        Local auth, rate-limit, and request-validation failures still use the standard Velvari `ErrorResponse` envelope.
      parameters:
        - name: profile
          in: path
          required: true
          schema:
            type: string
            enum:
              - driving-car
              - driving-hgv
              - cycling-regular
              - cycling-road
              - cycling-mountain
              - cycling-electric
              - foot-walking
              - foot-hiking
              - wheelchair
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ORSPassthroughJSON" }
      responses:
        "200":
          description: Upstream ORS directions JSON payload
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ORSPassthroughJSON" }
              examples:
                directions:
                  $ref: "#/components/examples/ORSDirectionsResponseExample"
        "400":
          description: Invalid request or upstream ORS validation error payload
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/ErrorResponse" }
                  - { $ref: "#/components/schemas/ORSPassthroughJSON" }
        "401":
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "404":
          description: Upstream ORS not-found payload
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ORSPassthroughJSON" }
        "429":
          description: Rate limited locally or by upstream ORS
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/ErrorResponse" }
                  - { $ref: "#/components/schemas/ORSPassthroughJSON" }
        "500":
          description: Server error or upstream ORS error payload
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/ErrorResponse" }
                  - { $ref: "#/components/schemas/ORSPassthroughJSON" }
        "503":
          description: Upstream unavailable, ORS not configured, or upstream ORS error payload
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/ErrorResponse" }
                  - { $ref: "#/components/schemas/ORSPassthroughJSON" }

  /v1/ors/matrix/{profile}:
    post:
      tags: [OpenRouteService]
      operationId: postORSMatrix
      summary: ORS matrix proxy
      description: |
        Thin proxy to OpenRouteService matrix v2.

        The request body and successful response payloads are passed through as JSON.
        If ORS returns an HTTP error response, that upstream JSON payload is also passed through with the upstream status code.
        Local auth, rate-limit, and request-validation failures still use the standard Velvari `ErrorResponse` envelope.
      parameters:
        - name: profile
          in: path
          required: true
          schema:
            type: string
            enum:
              - driving-car
              - driving-hgv
              - cycling-regular
              - cycling-road
              - cycling-mountain
              - cycling-electric
              - foot-walking
              - foot-hiking
              - wheelchair
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ORSPassthroughJSON" }
      responses:
        "200":
          description: Upstream ORS matrix JSON payload
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ORSPassthroughJSON" }
              examples:
                matrix:
                  $ref: "#/components/examples/ORSMatrixResponseExample"
        "400":
          description: Invalid request or upstream ORS validation error payload
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/ErrorResponse" }
                  - { $ref: "#/components/schemas/ORSPassthroughJSON" }
        "401":
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "404":
          description: Upstream ORS not-found payload
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ORSPassthroughJSON" }
        "429":
          description: Rate limited locally or by upstream ORS
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/ErrorResponse" }
                  - { $ref: "#/components/schemas/ORSPassthroughJSON" }
        "500":
          description: Server error or upstream ORS error payload
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/ErrorResponse" }
                  - { $ref: "#/components/schemas/ORSPassthroughJSON" }
        "503":
          description: Upstream unavailable, ORS not configured, or upstream ORS error payload
          content:
            application/json:
              schema:
                oneOf:
                  - { $ref: "#/components/schemas/ErrorResponse" }
                  - { $ref: "#/components/schemas/ORSPassthroughJSON" }

  /v2/cards/taxi:
    get:
      tags: [Taxi Cards v2]
      operationId: searchTaxiCardsV2
      summary: Search taxi cards (v2)
      description: |
        Strict machine-friendly taxi-card contract for new integrations.

        Provide zero or one of `q`, `id`, `number`, `registration_mark`, or `vin`.
        The endpoint also accepts documented compatibility aliases for current clients.
        Use `valid` to filter MTR validity status. Runtime also accepts `kehtiv` and `validity_status`.
      parameters:
        - name: q
          in: query
          schema: { type: string }
          description: Taxi-card holder/driver name search.
        - name: query
          in: query
          deprecated: true
          schema: { type: string }
          description: Alias for `q`.
        - name: holder_name
          in: query
          deprecated: true
          schema: { type: string }
          description: Alias for `q`.
        - name: id
          in: query
          schema: { type: string }
          description: Taxi-card id, for example `mtr:taxi-card:number:TVSK037464`.
        - name: number
          in: query
          schema: { type: string }
          description: Convenience alias mapped to `id=mtr:taxi-card:number:{number}`.
        - name: registration_mark
          in: query
          schema: { type: string }
          description: Vehicle registration mark.
        - name: reg_mark
          in: query
          deprecated: true
          schema: { type: string }
          description: Alias for `registration_mark`.
        - name: vin
          in: query
          schema: { type: string }
        - name: valid
          in: query
          schema:
            type: string
            example: "5"
          description: |
            MTR Kehtiv filter (English query name).
            1 / kehtiv / Jah = Yes (valid / active)
            2 / arhiveerimata / Kõik arhiveerimata kirjed sõltumata kehtivusest = All non-archived records regardless of validity
            3 / esitatud / Tulevikus = In the future (not yet valid)
            4 / peatatud / Ei (peatatud) = No (suspended)
            5 / arhiveeritud / Ei (arhiveeritud) = No (archived)
            6 / tahtajaline_loobumine / Ajutine loobumine = Temporary withdrawal
            7 / tahtajatu_loobumine / Tähtajatu loobumine = Indefinite withdrawal
            8 / 0 / Kõik = All statuses
        - name: kehtiv
          in: query
          deprecated: true
          schema:
            type: string
          description: Alias for `valid`.
        - name: validity_status
          in: query
          deprecated: true
          schema:
            type: string
          description: Alias for `valid`.
        - name: include_raw
          in: query
          schema: { type: boolean, default: false }
          description: Include raw upstream values for debugging/traceability.
        - name: include_vehicle
          in: query
          schema: { type: boolean, default: false }
          description: >
            Enrich vehicle make/model/seats via an extra vehicle lookup (slower).
            Registration-mark searches already include enrichment by default.
        - name: limit
          in: query
          schema: { type: integer, default: 20, maximum: 100 }
        - name: cursor
          in: query
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TaxiCardsV2Response" }
              examples:
                taxiCards:
                  $ref: "#/components/examples/TaxiCardsV2ResponseExample"
        "400":
          description: Invalid request
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "401":
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "429":
          description: Rate limited
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "500":
          description: Server error
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "503":
          description: Upstream unavailable
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /v2/cards/taxi/{id}:
    get:
      tags: [Taxi Cards v2]
      operationId: getTaxiCardV2
      summary: Taxi card by id (v2)
      description: Card-style lookup by taxi-card identifier using the v2 response contract.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: include_raw
          in: query
          schema: { type: boolean, default: false }
          description: Include raw upstream values for debugging/traceability.
        - name: include_vehicle
          in: query
          schema: { type: boolean, default: false }
          description: >
            Enrich vehicle make/model/seats via an extra vehicle lookup (slower).
            Registration-mark searches already include enrichment by default.
        - name: limit
          in: query
          schema: { type: integer, default: 20, maximum: 100 }
        - name: cursor
          in: query
          schema: { type: string }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TaxiCardsV2Response" }
              examples:
                taxiCard:
                  $ref: "#/components/examples/TaxiCardV2ResponseExample"
        "400":
          description: Invalid request
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "401":
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "429":
          description: Rate limited
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "500":
          description: Server error
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "503":
          description: Upstream unavailable
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
components:
  securitySchemes:
    ApiKeyHeaderAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: >
        Send one enabled key from the server-side `API_KEY_<LABEL>` set in the `X-API-Key` header.
    BearerApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: >
        Send the same server-issued API key as `Authorization: Bearer <key>`.
  examples:
    SunoProfileSongsResponseExample:
      summary: Cached Suno profile songs
      value:
        profile:
          handle: akaki
          url: https://suno.com/@akaki
          songs_url: https://suno.com/@akaki?page=songs
        items:
          - id: 132ec409-86b3-4587-87f8-a32ae30446d3
            title: Night Ride
            url: https://suno.com/song/132ec409-86b3-4587-87f8-a32ae30446d3
            cover_image_url: https://cdn2.suno.ai/cover.jpeg
            image_large_url: https://cdn2.suno.ai/cover-large.jpeg
            audio_url: https://cdn1.suno.ai/132ec409-86b3-4587-87f8-a32ae30446d3.mp3
            video_url: https://cdn1.suno.ai/132ec409-86b3-4587-87f8-a32ae30446d3.mp4
            video_cover_url: null
            created_at: "2026-05-03T07:00:00Z"
            type: gen
            task: null
            is_remix: false
            model: v5
            major_model_version: v5
            model_name: chirp-crow
            style_prompt: electro-pop
            lyrics: "[Verse] words"
            display_tags: electro-pop, dance
            caption: A public song caption.
            duration: 123.45
            duration_seconds: 123.45
            play_count: 10
            upvote_count: 2
            comment_count: 1
            has_hook: false
            hook_preview_thumbnail_url: null
            is_public: true
            is_pinned: false
            is_instrumental: false
            discovered_at: "2026-05-03T07:00:00Z"
            updated_at: "2026-05-03T07:00:00Z"
        count: 1
        updated_at: "2026-05-03T07:00:00Z"
        source: suno_public_profile
        diagnostics:
          html_bytes: 0
          profile_api_bytes: 226580
          direct_song_links_found: 0
          song_anchor_items_found: 0
          profile_api_song_items_found: 20
          profile_api_total_clips_count: 41
          uuid_candidates_found: 0
          excluded_null_uuid: false
          extraction_strategy: anonymous_unified_feed_user_songs
          target_user_id_found: true
          unified_feed_enabled: true
          unified_feed_auth_required: false
          unified_feed_cursor_strategy: top_level_cursor
          unified_feed_page_size: 20
          unified_feed_pages_fetched: 3
          unified_feed_next_cursor_last: "40"
          unified_feed_items_found: 41
          feed_items_skipped: 0
          final_unique_song_count: 41
          snapshot_written: true
          sync_warning: null
    SunoSongSearchResponseExample:
      summary: Cached Suno song search
      value:
        profile:
          handle: akaki
          url: https://suno.com/@akaki
          songs_url: https://suno.com/@akaki?page=songs
        query:
          artist: akaki
          title: Night Ride
          exact: true
        items:
          - id: 132ec409-86b3-4587-87f8-a32ae30446d3
            title: Night Ride
            url: https://suno.com/song/132ec409-86b3-4587-87f8-a32ae30446d3
            cover_image_url: https://cdn2.suno.ai/cover.jpeg
            image_large_url: https://cdn2.suno.ai/cover-large.jpeg
            audio_url: https://cdn1.suno.ai/132ec409-86b3-4587-87f8-a32ae30446d3.mp3
            video_url: https://cdn1.suno.ai/132ec409-86b3-4587-87f8-a32ae30446d3.mp4
            video_cover_url: null
            created_at: "2026-05-03T07:00:00Z"
            type: gen
            task: null
            is_remix: false
            model: v5
            major_model_version: v5
            model_name: chirp-crow
            style_prompt: electro-pop
            lyrics: "[Verse] words"
            display_tags: electro-pop, dance
            caption: A public song caption.
            duration: 123.45
            duration_seconds: 123.45
            play_count: 10
            upvote_count: 2
            comment_count: 1
            has_hook: false
            hook_preview_thumbnail_url: null
            is_public: true
            is_pinned: false
            is_instrumental: false
            discovered_at: "2026-05-03T07:00:00Z"
            updated_at: "2026-05-03T07:00:00Z"
        count: 1
        snapshot_updated_at: "2026-05-03T07:00:00Z"
        source: suno_public_profile
    SunoSongLookupResponseExample:
      summary: Cached Suno song by ID
      value:
        profile:
          handle: akaki
          url: https://suno.com/@akaki
          songs_url: https://suno.com/@akaki?page=songs
        item:
          id: 132ec409-86b3-4587-87f8-a32ae30446d3
          title: Night Ride
          url: https://suno.com/song/132ec409-86b3-4587-87f8-a32ae30446d3
          cover_image_url: https://cdn2.suno.ai/cover.jpeg
          image_large_url: https://cdn2.suno.ai/cover-large.jpeg
          audio_url: https://cdn1.suno.ai/132ec409-86b3-4587-87f8-a32ae30446d3.mp3
          video_url: https://cdn1.suno.ai/132ec409-86b3-4587-87f8-a32ae30446d3.mp4
          video_cover_url: null
          created_at: "2026-05-03T07:00:00Z"
          type: gen
          task: null
          is_remix: false
          model: v5
          major_model_version: v5
          model_name: chirp-crow
          style_prompt: electro-pop
          lyrics: "[Verse] words"
          display_tags: electro-pop, dance
          caption: A public song caption.
          duration: 123.45
          duration_seconds: 123.45
          play_count: 10
          upvote_count: 2
          comment_count: 1
          has_hook: false
          hook_preview_thumbnail_url: null
          is_public: true
          is_pinned: false
          is_instrumental: false
          discovered_at: "2026-05-03T07:00:00Z"
          updated_at: "2026-05-03T07:00:00Z"
        snapshot_updated_at: "2026-05-03T07:00:00Z"
        source: suno_public_profile
    SunoProfileStatsResponseExample:
      summary: Cached Suno profile stats
      value:
        profile:
          handle: akaki
          url: https://suno.com/@akaki
          songs_url: https://suno.com/@akaki?page=songs
        count: 41
        total_play_count: 1200
        total_upvote_count: 150
        total_comment_count: 30
        average_duration_seconds: 142.5
        instrumental_count: 4
        remix_count: 2
        pinned_count: 1
        hook_count: 3
        model_counts:
          v5: 25
          v4.5: 16
        style_counts:
          electro-pop: 12
          dance: 8
        top_by_play_count: []
        top_by_upvote_count: []
        top_by_comment_count: []
        snapshot_updated_at: "2026-05-03T07:00:00Z"
        source: suno_public_profile
    ORSRouteSummaryExample:
      summary: Normalized route summary
      value:
        profile: driving-car
        distance_m: 12834.7
        duration_s: 1042.5
        geometry:
          type: LineString
          coordinates:
            - [24.7536, 59.4370]
            - [24.7741, 59.4309]
    ORSAutocompleteResponseExample:
      summary: Address autocomplete suggestions
      value:
        suggestions:
          - label: Narva mnt 12, Johvi, Estonia
            name: Narva mnt 12
            country: Estonia
            region: Ida-Viru County
            locality: Johvi
            street: Narva mnt
            lat: 59.3579
            lng: 27.4129
    ORSReverseResponseExample:
      summary: Reverse geocode result
      value:
        label: Narva mnt 12, Johvi, Estonia
        suggestion:
          label: Narva mnt 12, Johvi, Estonia
          lat: 59.3579
          lng: 27.4129
    ORSIsochronesResponseExample:
      summary: ORS isochrones GeoJSON
      value:
        type: FeatureCollection
        features:
          - type: Feature
            properties:
              group_index: 0
              value: 300
            geometry:
              type: Polygon
              coordinates:
                - - [24.7536, 59.4370]
                  - [24.7600, 59.4400]
                  - [24.7700, 59.4350]
                  - [24.7536, 59.4370]
    ORSDirectionsResponseExample:
      summary: ORS directions payload
      value:
        routes:
          - summary:
              distance: 12834.7
              duration: 1042.5
            geometry: encoded-polyline
            segments:
              - distance: 12834.7
                duration: 1042.5
                steps: []
    ORSMatrixResponseExample:
      summary: ORS matrix payload
      value:
        durations:
          - [0, 1042.5]
          - [1038.2, 0]
        distances:
          - [0, 12834.7]
          - [12790.1, 0]
        sources:
          - location: [24.7536, 59.4370]
            name: Tallinn
        destinations:
          - location: [24.7741, 59.4309]
            name: Tallinn
    TaxiCardsV2ResponseExample:
      summary: Taxi-card search response
      value:
        meta:
          api_version: v2
          source:
            provider: mtr
            fetched_at: "2026-05-04T09:30:00Z"
          base_url: https://api.velvari.ee
        items:
          - id: mtr:taxi-card:number:TVSK037464
            source:
              record_id: "123456"
            number: TVSK037464
            status:
              code: valid
              source_text: Kehtiv
            validity:
              start_date: "2025-01-01"
              end_date: null
              is_indefinite: true
              archived_at: null
            driver:
              name: Example Driver
              phone: "+372 5555 0101"
              phone_raw: "+372 5555 0101"
              phone_e164: "+37255550101"
              email: driver@example.com
              driver_card_number: DT000001
            vehicle:
              registration_plate: 123ABC
              vin: TMBEXAMPLE00000001
              make: Skoda
              model: Octavia
              manufacture_year: "2021"
              seats: "5"
              emission_standard: Euro 6
              category: M1
              owner: Example Transport OU
            license:
              number: TSL000001
              owner:
                name: Example Transport OU
                registry_code: "12345678"
            licenses:
              - number: TSL000001
                owner:
                  name: Example Transport OU
                  registry_code: "12345678"
            issuer:
              name: Transpordiamet
            region:
              name: Harju maakond
            activity:
              code: taxi_service_vehicle_card
              label: Taxi service vehicle card
            features:
              ride_sharing: false
            links:
              self: /v2/cards/taxi/mtr%3Ataxi-card%3Anumber%3ATVSK037464
              detail: https://mtr.ttja.ee/taotluse_tulemus/123456
        paging:
          next_cursor: null
    TaxiCardV2ResponseExample:
      summary: Taxi-card lookup response
      value:
        meta:
          api_version: v2
          source:
            provider: mtr
            fetched_at: "2026-05-04T09:30:00Z"
        items:
          - id: mtr:taxi-card:number:TVSK037464
            source:
              record_id: "123456"
            number: TVSK037464
            status:
              code: valid
              source_text: Kehtiv
            validity:
              start_date: "2025-01-01"
              end_date: null
              is_indefinite: true
              archived_at: null
            driver:
              name: Example Driver
            vehicle:
              registration_plate: 123ABC
            license:
              number: TSL000001
              owner:
                name: Example Transport OU
                registry_code: "12345678"
            issuer:
              name: Transpordiamet
            region:
              name: Harju maakond
            activity:
              code: taxi_service_vehicle_card
              label: Taxi service vehicle card
            features:
              ride_sharing: false
            links:
              self: /v2/cards/taxi/mtr%3Ataxi-card%3Anumber%3ATVSK037464
              detail: https://mtr.ttja.ee/taotluse_tulemus/123456
        paging:
          next_cursor: null
  schemas:
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
            message:
              type: string
    SunoProfile:
      type: object
      required:
        - handle
        - url
        - songs_url
      properties:
        handle:
          type: string
          example: akaki
        url:
          type: string
          format: uri
          example: https://suno.com/@akaki
        songs_url:
          type: string
          format: uri
          example: https://suno.com/@akaki?page=songs
    SunoSongSearchQuery:
      type: object
      required:
        - artist
        - exact
      properties:
        artist:
          type: string
          example: akaki
        title:
          type: string
          example: Night Ride
        style:
          type: string
          example: electro-pop
        lyrics:
          type: string
          example: neon
        model:
          type: string
          example: v5
        min_play_count:
          type: integer
          nullable: true
          minimum: 0
          example: 10
        sort:
          type: string
          example: "-play_count"
        limit:
          type: integer
          minimum: 0
          maximum: 200
          example: 10
        exact:
          type: boolean
          example: true
    SunoSongItem:
      type: object
      required:
        - id
        - title
        - url
        - cover_image_url
        - discovered_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
          example: 132ec409-86b3-4587-87f8-a32ae30446d3
        title:
          type: string
          nullable: true
          example: null
        url:
          type: string
          format: uri
          example: https://suno.com/song/132ec409-86b3-4587-87f8-a32ae30446d3
        cover_image_url:
          type: string
          format: uri
          nullable: true
          example: null
        image_large_url:
          type: string
          format: uri
          nullable: true
          example: https://cdn2.suno.ai/cover-large.jpeg
        audio_url:
          type: string
          format: uri
          nullable: true
          example: https://cdn1.suno.ai/132ec409-86b3-4587-87f8-a32ae30446d3.mp3
        video_url:
          type: string
          format: uri
          nullable: true
          example: https://cdn1.suno.ai/132ec409-86b3-4587-87f8-a32ae30446d3.mp4
        video_cover_url:
          type: string
          format: uri
          nullable: true
          example: https://cdn1.suno.ai/video-cover.mp4
        created_at:
          type: string
          format: date-time
          nullable: true
          example: "2026-05-03T07:00:00Z"
        type:
          type: string
          nullable: true
          description: Optional Suno workflow type, read primarily from `content_item.metadata.type`, for example `gen`, `upsample`, or `edit_v3_export`. Omitted when Suno does not expose it.
          example: gen
        task:
          type: string
          nullable: true
          description: Optional Suno workflow task, read primarily from `content_item.metadata.task`, for example `cover`, `upsample`, or `chop_sample_condition`. Omitted when empty or unknown.
          example: cover
        is_remix:
          type: boolean
          nullable: true
          description: Optional Suno remix flag, read primarily from `content_item.metadata.is_remix`. Omitted when Suno does not expose it.
          example: true
        model:
          type: string
          nullable: true
          description: Legacy alias for `major_model_version`.
          example: v5
        major_model_version:
          type: string
          nullable: true
          description: Preferred model version field. Uses Suno `content_item.major_model_version` when present and falls back to `model` if Suno only exposes that field.
          example: v5
        model_name:
          type: string
          nullable: true
          example: chirp-crow
        style_prompt:
          type: string
          nullable: true
          description: Suno `metadata.tags`.
          example: electro-pop
        lyrics:
          type: string
          nullable: true
          description: Suno `metadata.prompt`.
          example: "[Verse] words"
        display_tags:
          type: string
          nullable: true
          example: electro-pop, dance
        caption:
          type: string
          nullable: true
          example: A public song caption.
        duration:
          type: number
          format: double
          nullable: true
          description: Legacy alias for `duration_seconds`.
          example: 123.45
        duration_seconds:
          type: number
          format: double
          nullable: true
          description: Preferred duration in seconds, read from Suno `content_item.metadata.duration` or `duration_seconds` and mirrored to `duration`.
          example: 123.45
        play_count:
          type: integer
          nullable: true
          example: 10
        upvote_count:
          type: integer
          nullable: true
          example: 2
        comment_count:
          type: integer
          nullable: true
          example: 1
        has_hook:
          type: boolean
          nullable: true
          example: false
        hook_preview_thumbnail_url:
          type: string
          format: uri
          nullable: true
          example: null
        is_public:
          type: boolean
          nullable: true
          example: true
        is_pinned:
          type: boolean
          nullable: true
          example: false
        is_instrumental:
          type: boolean
          nullable: true
          description: Optional Suno instrumental flag, read primarily from `content_item.metadata.make_instrumental` or `is_instrumental`. Omitted when Suno does not expose it.
          example: false
        discovered_at:
          type: string
          format: date-time
          example: "2026-05-03T07:00:00Z"
        updated_at:
          type: string
          format: date-time
          example: "2026-05-03T07:00:00Z"
    SunoDiagnostics:
      type: object
      required:
        - html_bytes
        - profile_api_bytes
        - direct_song_links_found
        - song_anchor_items_found
        - profile_api_song_items_found
        - profile_api_total_clips_count
        - uuid_candidates_found
        - excluded_null_uuid
        - extraction_strategy
        - target_user_id_found
        - unified_feed_enabled
        - unified_feed_auth_required
        - unified_feed_cursor_strategy
        - unified_feed_page_size
        - unified_feed_pages_fetched
        - unified_feed_next_cursor_last
        - unified_feed_items_found
        - feed_items_skipped
        - final_unique_song_count
        - snapshot_written
        - sync_warning
      properties:
        html_bytes:
          type: integer
          minimum: 0
          example: 0
        profile_api_bytes:
          type: integer
          minimum: 0
          example: 226580
        direct_song_links_found:
          type: integer
          minimum: 0
          example: 0
        song_anchor_items_found:
          type: integer
          minimum: 0
          example: 0
        profile_api_song_items_found:
          type: integer
          minimum: 0
          example: 20
        profile_api_total_clips_count:
          type: integer
          minimum: 0
          description: Public `stats.clips_count` from the Suno profile API when present.
          example: 41
        uuid_candidates_found:
          type: integer
          minimum: 0
          example: 0
        excluded_null_uuid:
          type: boolean
          example: false
        extraction_strategy:
          type: string
          example: anonymous_unified_feed_user_songs
        sync_warning:
          type: string
          nullable: true
          description: Null for a complete sync; set when a bounded sync may be incomplete, for example after reaching the max page cap.
          example: null
        target_user_id_found:
          type: boolean
          description: Whether the public profile response yielded the target user ID required by the unified feed.
          example: true
        unified_feed_enabled:
          type: boolean
          description: Whether sync used anonymous public unified feed pagination.
          example: true
        unified_feed_auth_required:
          type: boolean
          description: Expected to be false; this integration does not use private Suno auth.
          example: false
        unified_feed_cursor_strategy:
          type: string
          description: Cursor placement used for page requests.
          example: top_level_cursor
        unified_feed_page_size:
          type: integer
          minimum: 0
          description: Page size sent to Suno unified feed, configured by `SUNO_PAGE_SIZE`.
          example: 20
        unified_feed_pages_fetched:
          type: integer
          minimum: 0
          description: Number of unified feed pages fetched before an empty cursor, error, or max page cap.
          example: 3
        unified_feed_next_cursor_last:
          type: string
          nullable: true
          description: Last non-empty `next_cursor` observed. Null when the first page is terminal.
          example: "40"
        unified_feed_items_found:
          type: integer
          minimum: 0
          description: Raw unified feed items observed across fetched pages before filtering and deduplication.
          example: 41
        feed_items_skipped:
          type: integer
          minimum: 0
          description: Items skipped because they were hidden, trashed, private, not clips, not song schemas, or invalid IDs.
          example: 0
        final_unique_song_count:
          type: integer
          minimum: 0
          description: Final number of unique valid public songs after filtering and ID deduplication.
          example: 41
        snapshot_written:
          type: boolean
          description: Whether this sync result was atomically written. False when downgrade prevention kept a larger existing snapshot.
          example: true
    SunoProfileSongsResponse:
      type: object
      required:
        - profile
        - items
        - count
        - updated_at
        - source
        - diagnostics
      properties:
        profile:
          $ref: "#/components/schemas/SunoProfile"
        items:
          type: array
          items:
            $ref: "#/components/schemas/SunoSongItem"
        count:
          type: integer
          minimum: 0
          example: 1
        updated_at:
          type: string
          format: date-time
          example: "2026-05-03T07:00:00Z"
        source:
          type: string
          enum: [suno_public_profile]
        diagnostics:
          $ref: "#/components/schemas/SunoDiagnostics"
    SunoSongSearchResponse:
      type: object
      required:
        - profile
        - query
        - items
        - count
        - snapshot_updated_at
        - source
      properties:
        profile:
          $ref: "#/components/schemas/SunoProfile"
        query:
          $ref: "#/components/schemas/SunoSongSearchQuery"
        items:
          type: array
          items:
            $ref: "#/components/schemas/SunoSongItem"
        count:
          type: integer
          minimum: 0
          example: 1
        snapshot_updated_at:
          type: string
          format: date-time
          example: "2026-05-03T07:00:00Z"
        source:
          type: string
          enum: [suno_public_profile]
    SunoSongLookupResponse:
      type: object
      required:
        - profile
        - item
        - snapshot_updated_at
        - source
      properties:
        profile:
          $ref: "#/components/schemas/SunoProfile"
        item:
          $ref: "#/components/schemas/SunoSongItem"
        snapshot_updated_at:
          type: string
          format: date-time
          example: "2026-05-03T07:00:00Z"
        source:
          type: string
          enum: [suno_public_profile]
    SunoProfileStatsResponse:
      type: object
      required:
        - profile
        - count
        - total_play_count
        - total_upvote_count
        - total_comment_count
        - instrumental_count
        - remix_count
        - pinned_count
        - hook_count
        - model_counts
        - style_counts
        - top_by_play_count
        - top_by_upvote_count
        - top_by_comment_count
        - snapshot_updated_at
        - source
      properties:
        profile:
          $ref: "#/components/schemas/SunoProfile"
        count:
          type: integer
          example: 41
        total_play_count:
          type: integer
          example: 1200
        total_upvote_count:
          type: integer
          example: 150
        total_comment_count:
          type: integer
          example: 30
        average_duration_seconds:
          type: number
          format: double
          nullable: true
          example: 142.5
        instrumental_count:
          type: integer
          example: 4
        remix_count:
          type: integer
          example: 2
        pinned_count:
          type: integer
          example: 1
        hook_count:
          type: integer
          example: 3
        model_counts:
          type: object
          additionalProperties:
            type: integer
        style_counts:
          type: object
          additionalProperties:
            type: integer
        top_by_play_count:
          type: array
          items:
            $ref: "#/components/schemas/SunoSongItem"
        top_by_upvote_count:
          type: array
          items:
            $ref: "#/components/schemas/SunoSongItem"
        top_by_comment_count:
          type: array
          items:
            $ref: "#/components/schemas/SunoSongItem"
        snapshot_updated_at:
          type: string
          format: date-time
          example: "2026-05-03T07:00:00Z"
        source:
          type: string
          enum: [suno_public_profile]
    ORSGeometry:
      type: object
      required:
        - type
        - coordinates
      properties:
        type:
          type: string
          example: LineString
        coordinates:
          type: array
          items:
            type: array
            minItems: 2
            maxItems: 2
            items:
              type: number
              format: double
    ORSRouteSummary:
      type: object
      required:
        - profile
        - distance_m
        - duration_s
        - geometry
      properties:
        profile:
          type: string
          example: driving-car
        distance_m:
          type: number
          format: double
        duration_s:
          type: number
          format: double
        geometry:
          $ref: "#/components/schemas/ORSGeometry"
    ORSReverseSuggestion:
      type: object
      required:
        - label
        - lat
        - lng
      properties:
        label:
          type: string
          example: Narva mnt 12, Jõhvi, Estonia
        lat:
          type: number
          format: double
        lng:
          type: number
          format: double
    ORSReverseResponse:
      type: object
      required:
        - label
        - suggestion
      properties:
        label:
          type: string
          example: Narva mnt 12, Jõhvi, Estonia
        suggestion:
          $ref: "#/components/schemas/ORSReverseSuggestion"
    ORSAutocompleteSuggestion:
      type: object
      required:
        - label
        - lat
        - lng
      properties:
        label:
          type: string
        name:
          type: string
        country:
          type: string
        region:
          type: string
        locality:
          type: string
        street:
          type: string
        lat:
          type: number
          format: double
        lng:
          type: number
          format: double
    ORSAutocompleteResponse:
      type: object
      required:
        - suggestions
      properties:
        suggestions:
          type: array
          items:
            $ref: "#/components/schemas/ORSAutocompleteSuggestion"
    ORSPassthroughJSON:
      type: object
      description: Arbitrary JSON payload proxied from OpenRouteService. Exact shape depends on upstream service and request parameters.
      additionalProperties: true
    Source:
      type: object
      required:
        - provider
        - fetched_at
      properties:
        provider:
          type: string
        fetched_at:
          type: string
          format: date-time
    TaxiCardsV2Response:
      type: object
      required:
        - meta
        - items
        - paging
      properties:
        meta:
          $ref: "#/components/schemas/TaxiCardsV2Meta"
        items:
          type: array
          items:
            $ref: "#/components/schemas/TaxiCardV2"
        paging:
          $ref: "#/components/schemas/TaxiCardsV2Paging"
    TaxiCardsV2Meta:
      type: object
      required:
        - api_version
        - source
      properties:
        api_version:
          type: string
          enum: [v2]
        source:
          $ref: "#/components/schemas/Source"
        base_url:
          type: string
          format: uri
          description: Optional base URL for composing absolute URLs from relative `links.self`.
    TaxiCardsV2Paging:
      type: object
      required:
        - next_cursor
      properties:
        next_cursor:
          type: string
          nullable: true
    TaxiCardV2:
      type: object
      required:
        - id
        - source
        - status
        - validity
        - driver
        - vehicle
        - license
        - issuer
        - region
        - activity
        - features
        - links
      properties:
        id:
          type: string
        source:
          $ref: "#/components/schemas/TaxiCardV2ItemSource"
        number:
          type: string
          nullable: true
          description: Optional taxi-card number. Omitted when unknown.
        status:
          $ref: "#/components/schemas/TaxiCardV2Status"
        validity:
          $ref: "#/components/schemas/TaxiCardV2Validity"
        driver:
          $ref: "#/components/schemas/TaxiCardV2Driver"
        vehicle:
          $ref: "#/components/schemas/TaxiCardV2Vehicle"
        license:
          $ref: "#/components/schemas/TaxiCardV2License"
        licenses:
          type: array
          description: Additive normalized license list. `license` remains the primary (first) license for backward compatibility.
          items:
            $ref: "#/components/schemas/TaxiCardV2License"
        issuer:
          $ref: "#/components/schemas/TaxiCardV2Issuer"
        region:
          $ref: "#/components/schemas/TaxiCardV2Region"
        activity:
          $ref: "#/components/schemas/TaxiCardV2Activity"
        features:
          $ref: "#/components/schemas/TaxiCardV2Features"
        links:
          $ref: "#/components/schemas/TaxiCardV2Links"
        raw:
          $ref: "#/components/schemas/TaxiCardV2Raw"
    TaxiCardV2Status:
      type: object
      required:
        - code
      properties:
        code:
          type: string
          enum:
            - valid
            - expired
            - suspended
            - unknown
        source_text:
          type: string
    TaxiCardV2Validity:
      type: object
      required:
        - start_date
        - end_date
        - is_indefinite
      properties:
        start_date:
          type: string
          format: date
          nullable: true
        end_date:
          type: string
          format: date
          nullable: true
        is_indefinite:
          type: boolean
        archived_at:
          type: string
          format: date-time
          nullable: true
    TaxiCardV2ItemSource:
      type: object
      properties:
        record_id:
          type: string
          description: Upstream MTR record id from `/taotluse_tulemus/{id}` when available.
    TaxiCardV2Driver:
      type: object
      properties:
        name:
          type: string
        phone:
          type: string
        phone_raw:
          type: string
        phone_e164:
          type: string
        email:
          type: string
        driver_card_number:
          type: string
    TaxiCardV2Vehicle:
      type: object
      properties:
        registration_plate:
          type: string
        vin:
          type: string
        make:
          type: string
        model:
          type: string
        manufacture_year:
          type: string
        seats:
          type: string
        emission_standard:
          type: string
        category:
          type: string
        owner:
          type: string
    TaxiCardV2License:
      type: object
      required:
        - owner
      properties:
        number:
          type: string
        owner:
          $ref: "#/components/schemas/TaxiCardV2LicenseOwner"
        owner_status:
          type: string
          enum: [deleted]
    TaxiCardV2LicenseOwner:
      type: object
      properties:
        name:
          type: string
        registry_code:
          type: string
    TaxiCardV2Issuer:
      type: object
      properties:
        name:
          type: string
          nullable: true
    TaxiCardV2Region:
      type: object
      properties:
        name:
          type: string
    TaxiCardV2Activity:
      type: object
      required:
        - code
      properties:
        code:
          type: string
          enum:
            - taxi_service_vehicle_card
            - unknown
        label:
          type: string
    TaxiCardV2Features:
      type: object
      properties:
        ride_sharing:
          type: boolean
    TaxiCardV2Links:
      type: object
      properties:
        self:
          type: string
        detail:
          type: string
    TaxiCardV2Raw:
      type: object
      properties:
        status:
          type: string
        valid_until:
          type: string
        archiving_time:
          type: string
        ride_sharing_service:
          type: string
        additional_info:
          type: string
        taxi_license_owner:
          type: string
        area_of_activity:
          type: string
        operating_region:
          type: string
