openapi: 3.1.0
info:
  title: Skylens API
  version: '2026-08-12'
  summary: The project-scoped HTTP API of the Skylens Spatial OS.
  description: 'Every read and write is scoped to one project, and the browser never reaches a database
    directly.


    This document is GENERATED from `server/db/openapi.yaml` by `tools/api-docs/build_public_openapi.py`;
    it is the public projection of that contract and is never edited by hand. Operations that are internal,
    worker-facing, admin-only or merely planned are absent by construction, not by omission.


    The response shape is documented as implemented: successes are bare JSON objects carrying `correlationId`
    and `traceId`, and errors are `{"error": {code, message, retryable, correlationId}}`. The `{data,
    meta, error}` envelope described in `docs/contracts/API.md` is implemented only by the Partner Services
    API.'
  x-generated-by: tools/api-docs/build_public_openapi.py
  x-source-of-truth:
  - server/db/openapi.yaml
  - server/db/partner_openapi.yaml
servers:
- url: http://127.0.0.1:8600
  description: Local development
  x-skylens-environment: local
  x-skylens-configured: true
  x-skylens-try-it: true
  x-skylens-note: 'Started by `npm run dev`. Identity is the X-Skylens-User-Id header, trusted verbatim
    because no gateway fronts it. CORS is Access-Control-Allow-Origin: * on this surface.'
- url: https://api.example.skylens.co.il
  description: Production (PLACEHOLDER — requires deployment configuration)
  x-skylens-environment: production
  x-skylens-configured: false
  x-skylens-try-it: false
  x-skylens-note: 'PLACEHOLDER — requires deployment configuration. The gateway (functions/project-api-gateway.js)
    is implemented and unit-tested but not wired: firebase.json has no "/api/**" -> projectApiGateway
    rewrite, and SKYLENS_API_UPSTREAM_URL / SKYLENS_CLOUD_RUN_AUDIENCE / SKYLENS_GATEWAY_SHARED_SECRET
    are unset. Until then this host resolves to nothing and Try It is disabled against it.'
tags:
- name: projects
  x-display-name: Projects
  description: ''
- name: documents
  x-display-name: Documents
  description: ''
- name: gis
  x-display-name: GIS Layers and Features
  description: ''
- name: spatial-analysis
  x-display-name: Spatial Analysis
  description: ''
- name: elevation
  x-display-name: Elevation
  description: ''
- name: review
  x-display-name: Entity Review
  description: ''
- name: reality-models
  x-display-name: Reality Models
  description: ''
- name: imports
  x-display-name: CAD and BIM Imports
  description: ''
- name: photos
  x-display-name: Photos and 360 Panoramas
  description: ''
- name: ai-jobs
  x-display-name: AI Jobs
  description: ''
- name: agent-memory
  x-display-name: Agent Memory
  description: ''
- name: viewer
  x-display-name: Viewer
  description: ''
- name: chat-database
  x-display-name: Chat Database Grounding
  description: ''
- name: export
  x-display-name: Export
  description: ''
- name: partners
  x-display-name: Partner Services
  description: ''
paths:
  /api/projects/{projectId}/extraction-plans:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - ai-jobs
      summary: Create an immutable extraction plan
      responses:
        default:
          description: OK
      operationId: postProjectsByProjectIdExtractionPlans
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: ai-jobs
        surface: platform
        role: editor
        source: server/db/extraction_planner.create_plan
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/extraction-plans
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/extraction-plans/{planId}:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: planId
      in: path
      required: true
      schema:
        type: string
    get:
      tags:
      - ai-jobs
      summary: Get an extraction plan with its work units, cost estimate and current state.
      responses:
        default:
          description: OK
        '404':
          description: NOT_FOUND
      operationId: getProjectsByProjectIdExtractionPlansByPlanId
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: ai-jobs
        surface: platform
        role: editor
        source: server/db/extraction_orchestrator.get_plan
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/extraction-plans/77777777-7777-4777-8777-777777777777
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/extraction-plans/{planId}/confirm:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: planId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - ai-jobs
      summary: Confirm a plan so a job may be started from it. Body {idempotencyKey} is REQUIRED.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdempotentCommand'
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_ARGUMENT
        '409':
          description: INVALID_STATE | IDEMPOTENCY_CONFLICT
        '410':
          description: PLAN_EXPIRED
      operationId: postProjectsByProjectIdExtractionPlansByPlanIdConfirm
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: ai-jobs
        surface: platform
        role: editor
        idempotency: body
        source: server/db/extraction_orchestrator.confirm_plan
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/extraction-plans/77777777-7777-4777-8777-777777777777/confirm
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/extraction-plans/{planId}/cancel:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: planId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - ai-jobs
      summary: Cancel an unconfirmed plan. Body {idempotencyKey} is REQUIRED.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdempotentCommand'
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_ARGUMENT
        '409':
          description: INVALID_STATE
      operationId: postProjectsByProjectIdExtractionPlansByPlanIdCancel
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: ai-jobs
        surface: platform
        role: editor
        idempotency: body
        source: server/db/extraction_orchestrator.cancel_plan
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/extraction-plans/77777777-7777-4777-8777-777777777777/cancel
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/extraction-jobs:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    get:
      tags:
      - ai-jobs
      summary: List extraction jobs
      responses:
        default:
          description: OK
      operationId: getProjectsByProjectIdExtractionJobs
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: ai-jobs
        surface: platform
        role: editor
        source: server/db/extraction_orchestrator.list_jobs
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/extraction-jobs
        sourceDocument: server/db/openapi.yaml
    post:
      tags:
      - ai-jobs
      summary: Start a job from a confirmed plan
      responses:
        default:
          description: OK
      operationId: postProjectsByProjectIdExtractionJobs
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: ai-jobs
        surface: platform
        role: editor
        idempotency: body
        source: server/db/extraction_orchestrator.start_job
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/extraction-jobs
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/extraction-jobs/{jobId}:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: jobId
      in: path
      required: true
      schema:
        type: string
    get:
      tags:
      - ai-jobs
      summary: Get a job with its per-task progress, lease state and terminal outcome.
      responses:
        default:
          description: OK
        '404':
          description: NOT_FOUND
      operationId: getProjectsByProjectIdExtractionJobsByJobId
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: ai-jobs
        surface: platform
        role: editor
        source: server/db/extraction_orchestrator.get_job
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/extraction-jobs/66666666-6666-4666-8666-666666666666
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/extraction-jobs/{jobId}/cancel:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: jobId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - ai-jobs
      summary: Cancel a non-terminal job. Body {idempotencyKey} is REQUIRED.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdempotentCommand'
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_ARGUMENT
        '409':
          description: INVALID_STATE
      operationId: postProjectsByProjectIdExtractionJobsByJobIdCancel
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: ai-jobs
        surface: platform
        role: editor
        idempotency: body
        source: server/db/extraction_orchestrator.cancel_job
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/extraction-jobs/66666666-6666-4666-8666-666666666666/cancel
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/extraction-jobs/{jobId}/retry:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: jobId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - ai-jobs
      summary: Retry a failed job. Body {idempotencyKey} is REQUIRED.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdempotentCommand'
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_ARGUMENT
        '409':
          description: INVALID_STATE | RETRY_EXHAUSTED
      operationId: postProjectsByProjectIdExtractionJobsByJobIdRetry
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: ai-jobs
        surface: platform
        role: editor
        idempotency: body
        source: server/db/extraction_orchestrator.retry_job
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/extraction-jobs/66666666-6666-4666-8666-666666666666/retry
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/drafts/merge:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - review
      summary: Merge draft features (body featureIds)
      responses:
        default:
          description: OK
      operationId: postProjectsByProjectIdDraftsMerge
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: review
        surface: platform
        role: reviewer
        idempotency: body
        concurrency: expectedRevisions
        source: server/db/review_service.merge_features
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/drafts/merge
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/drafts/{draftId}:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: draftId
      in: path
      required: true
      schema:
        type: string
    get:
      tags:
      - review
      summary: Get one draft feature with its full revision history.
      responses:
        default:
          description: OK
        '404':
          description: NOT_FOUND
      operationId: getProjectsByProjectIdDraftsByDraftId
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: review
        surface: platform
        role: viewer
        source: server/db/review_service.get_feature_history
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/drafts/55555555-5555-4555-8555-555555555555
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/drafts/{draftId}/geometry:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: draftId
      in: path
      required: true
      schema:
        type: string
    patch:
      tags:
      - review
      summary: Replace a draft's geometry (editor). Body {expectedRevision, geometry, reason, idempotencyKey}
        — all required.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DraftGeometryEdit'
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_ARGUMENT
        '403':
          description: FORBIDDEN
        '409':
          description: VERSION_CONFLICT
      operationId: patchProjectsByProjectIdDraftsByDraftIdGeometry
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: review
        surface: platform
        role: editor
        idempotency: body
        concurrency: expectedRevision
        crs: GeoJSON EPSG:4326
        source: server/db/review_service.edit_geometry
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/drafts/55555555-5555-4555-8555-555555555555/geometry
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/drafts/{draftId}/approve:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: draftId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - review
      summary: Approve a draft into an authoritative feature (REVIEWER). Body {expectedRevision, reason,
        idempotencyKey, observedAt?}.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DraftApprove'
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_ARGUMENT
        '403':
          description: FORBIDDEN — needs the reviewer role
        '409':
          description: VERSION_CONFLICT
      operationId: postProjectsByProjectIdDraftsByDraftIdApprove
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: review
        surface: platform
        role: reviewer
        idempotency: body
        concurrency: expectedRevision
        source: server/db/review_service.approve_feature
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/drafts/55555555-5555-4555-8555-555555555555/approve
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/drafts/{draftId}/reject:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: draftId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - review
      summary: Reject a draft (REVIEWER). Body {expectedRevision, reason, idempotencyKey}.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DraftDecision'
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_ARGUMENT
        '403':
          description: FORBIDDEN
        '409':
          description: VERSION_CONFLICT
      operationId: postProjectsByProjectIdDraftsByDraftIdReject
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: review
        surface: platform
        role: reviewer
        idempotency: body
        concurrency: expectedRevision
        source: server/db/review_service.reject_feature
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/drafts/55555555-5555-4555-8555-555555555555/reject
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/drafts/{draftId}/split:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: draftId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - review
      summary: 'Split one draft into several (REVIEWER). Body {expectedRevision, geometries, reason, idempotencyKey}.
        Stable-ID rules apply: the parent''s identity is preserved in lineage.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DraftSplit'
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_ARGUMENT
        '403':
          description: FORBIDDEN
        '409':
          description: VERSION_CONFLICT
      operationId: postProjectsByProjectIdDraftsByDraftIdSplit
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: review
        surface: platform
        role: reviewer
        idempotency: body
        concurrency: expectedRevision
        source: server/db/review_service.split_feature
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/drafts/55555555-5555-4555-8555-555555555555/split
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/drafts/{draftId}/reclassify:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: draftId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - review
      summary: Move a draft to another layer / entity type (REVIEWER). Body {expectedRevision, targetLayerId,
        targetEntityTypeId, reason, idempotencyKey}.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DraftReclassify'
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_ARGUMENT
        '403':
          description: FORBIDDEN
        '409':
          description: VERSION_CONFLICT
      operationId: postProjectsByProjectIdDraftsByDraftIdReclassify
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: review
        surface: platform
        role: reviewer
        idempotency: body
        concurrency: expectedRevision
        source: server/db/review_service.reclassify_feature
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/drafts/55555555-5555-4555-8555-555555555555/reclassify
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/drafts/{draftId}/regenerate:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: draftId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - review
      summary: Queue an AI regeneration of one draft (async). Body {expectedRevision, reason, idempotencyKey,
        options?}.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DraftRegenerate'
      responses:
        '202':
          description: Accepted — regeneration queued
        '400':
          description: INVALID_ARGUMENT
        '403':
          description: FORBIDDEN
        '409':
          description: VERSION_CONFLICT
      operationId: postProjectsByProjectIdDraftsByDraftIdRegenerate
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: review
        surface: platform
        role: reviewer
        idempotency: body
        concurrency: expectedRevision
        source: server/db/review_service.regenerate_feature
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/drafts/55555555-5555-4555-8555-555555555555/regenerate
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/review-queue:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    get:
      tags:
      - review
      summary: List the review queue
      responses:
        default:
          description: OK
      operationId: getProjectsByProjectIdReviewQueue
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: review
        surface: platform
        role: reviewer
        source: server/db/review_service.list_review_queue
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/review-queue
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/my-layers:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    get:
      tags:
      - viewer
      summary: Snapshot of the project's user-authored layers, their features, the saved view state and
        the project AOIs. A layer is SAVED with PUT .../my-layers/{layerId} — there is no POST on this
        collection (extraction_api.py returns 405).
      responses:
        default:
          description: OK
      operationId: getProjectsByProjectIdMyLayers
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: viewer
        surface: platform
        role: editor
        source: server/db/extraction_repository.get_my_layers_snapshot
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/my-layers
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/my-layers/view-state:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - viewer
      summary: 'Save which layers are active. Body {activeLayers: string[]} (an array is required). POST,
        not PUT — PUT is 405 (a baseline declaration of `put` here was never implemented).'
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_ARGUMENT (activeLayers is not an array)
      operationId: postProjectsByProjectIdMyLayersViewState
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: viewer
        surface: platform
        role: editor
        source: server/db/extraction_repository.save_project_layer_view_state
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/my-layers/view-state
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/my-layers/{layerId}:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: layerId
      in: path
      required: true
      schema:
        type: string
    put:
      tags:
      - viewer
      summary: Create or replace one user-authored layer and its features, under optimistic concurrency.
        layer.layerId MUST equal the path segment; layer.kind is user_draw or entity_layer; layer.provenance.source
        must be user-authored (user_draw/draw_menu, or approved_building_review for an entity_layer) —
        the route refuses to let a client forge an authoritative provenance. Every feature needs featureId
        and geometry. There is no GET on this path (405); read it from the my-layers snapshot.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - layer
              properties:
                expectedRevision:
                  type: integer
                  nullable: true
                  description: Optimistic concurrency; a stale value is 409 VERSION_CONFLICT.
                layer:
                  type: object
                features:
                  type: array
                  items:
                    type: object
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_ARGUMENT (layerId mismatch, unsupported kind, non-user provenance, malformed
            feature)
        '409':
          description: VERSION_CONFLICT
      operationId: putProjectsByProjectIdMyLayersByLayerId
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: viewer
        surface: platform
        role: editor
        concurrency: expectedRevision
        bodyLimitMb: 32
        source: server/db/extraction_repository.upsert_project_layer
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/my-layers/layer_demo_001
        sourceDocument: server/db/openapi.yaml
    delete:
      tags:
      - viewer
      summary: Delete one user-authored layer. Body {expectedRevision} is REQUIRED.
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_ARGUMENT
        '409':
          description: VERSION_CONFLICT
      operationId: deleteProjectsByProjectIdMyLayersByLayerId
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: viewer
        surface: platform
        role: editor
        concurrency: expectedRevision
        source: server/db/extraction_repository.delete_project_layer
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/my-layers/layer_demo_001
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/trusted-features:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    get:
      tags:
      - gis
      summary: List authoritative/trusted features
      responses:
        default:
          description: OK
      operationId: getProjectsByProjectIdTrustedFeatures
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: gis
        surface: platform
        role: viewer
        source: server/db/review_service.list_trusted_features
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/trusted-features
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/layer-features:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    get:
      tags:
      - gis
      summary: Query layer features (bbox/crs)
      responses:
        default:
          description: OK
      operationId: getProjectsByProjectIdLayerFeatures
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: gis
        surface: platform
        role: viewer
        crs: crs=4326 (default) or crs=project
        source: server/db/export_service.query_features
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/layer-features
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/db-catalog:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: includeCounts
      in: query
      required: false
      schema:
        type: string
      description: '''true'' adds per-domain live record counts (server-cached ~30s)'
    get:
      tags:
      - chat-database
      summary: 'Resolved chat exposure catalog for the project (viewer role; skill: skylens-chat-database).
        Read-only. Domains, filters, aggregates, aliases and optional counts.'
      responses:
        default:
          description: OK
        '403':
          description: FORBIDDEN
        '404':
          description: NOT_FOUND
      operationId: getProjectsByProjectIdDbCatalog
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: chat-database
        surface: platform
        role: viewer
        source: server/db/chat_db_query_service.get_catalog
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/db-catalog
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/db-query:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - chat-database
      summary: 'Dispatch one frozen-SQL chat-db tool (db_count, db_describe, db_list_models, db_query_gis_features,
        db_query_entities, db_query_relations, db_spatial_relate, db_list_layers). project_id is bound
        from the path, never from input; read-only; viewer role. Body: {contractVersion, tool, input}.'
      responses:
        default:
          description: OK
        '400':
          description: UNKNOWN_TOOL / UNKNOWN_DOMAIN / UNKNOWN_FILTER / SCOPE_TOO_LARGE / INVALID_ARGUMENT
        '403':
          description: FORBIDDEN
        '404':
          description: NOT_FOUND
        '500':
          description: QUERY_TIMEOUT (retryable)
      operationId: postProjectsByProjectIdDbQuery
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: chat-database
        surface: platform
        role: viewer
        source: server/db/chat_db_query_service.dispatch_tool
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/db-query
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/models/{modelId}/thumbnail:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: modelId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    put:
      tags:
      - reality-models
      summary: 'Persist a small client-captured preview onto the model''s metadata.thumbnailUrl (editor
        role). Body: {thumbnailUrl} — an image data: URL, <=64KB. Surfaced by GET /models for project
        cards.'
      responses:
        default:
          description: OK
        '400':
          description: 'INVALID_ARGUMENT (not an image data: URL / too large / bad modelId)'
        '403':
          description: FORBIDDEN
        '404':
          description: NOT_FOUND
      operationId: putProjectsByProjectIdModelsByModelIdThumbnail
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: reality-models
        surface: platform
        role: editor
        source: server/db/reality_model_service.set_model_thumbnail
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/models/44444444-4444-4444-8444-444444444444/thumbnail
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/reality-models/{modelId}/delivery:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    - name: modelId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    - name: ttlSeconds
      in: query
      required: false
      schema:
        type: integer
        minimum: 60
        maximum: 900
        default: 900
      description: May only SHORTEN the grant; a larger value is capped, never honoured.
    get:
      tags:
      - reality-models
      summary: 'Issue a short-lived CloudFront signed-cookie delivery grant for ONE reality model (ADR
        0028; action reality.read; flag SKYLENS_REALITY_SIGNED_DELIVERY_ENABLED, default OFF => 404).
        The three CloudFront cookies are returned as Set-Cookie headers (HttpOnly, Secure, SameSite=None,
        Domain=<shared parent>, Path=<model prefix>) and NEVER in the body; the viewer never reads a cookie
        value. Body: {kind, delivery, accessUrl, trustHost, cookieScope:{domain,path}, expiresAt, ttlSeconds}
        — no bucket, key, prefix or credential. The organization, the storage prefix and the CDN scope
        are derived server-side from the model row: a caller-supplied organizationId/prefix/bucket is
        400 UNKNOWN_FIELD, and a model that is not in {projectId} is 404, indistinguishable from one that
        does not exist.'
      responses:
        '200':
          description: 'Grant issued; response carries Set-Cookie x3 and Cache-Control: private, no-store'
        '400':
          description: UNKNOWN_FIELD (a client-supplied scope) / INVALID_ARGUMENT (ttlSeconds)
        '403':
          description: FORBIDDEN (member without reality.read)
        '404':
          description: NOT_FOUND (flag off, project undisclosed, or model not in this project)
        '405':
          description: METHOD_NOT_ALLOWED
        '500':
          description: INTERNAL (signing or the delivery audit trail is unavailable — no grant is issued)
      operationId: getProjectsByProjectIdRealityModelsByModelIdDelivery
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: reality-models
        surface: platform
        permission: reality.read
        flag: SKYLENS_REALITY_SIGNED_DELIVERY_ENABLED
        source: server/db/reality_delivery_service.issue_model_delivery
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/reality-models/44444444-4444-4444-8444-444444444444/delivery
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/gis-layers:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: theme
      in: query
      required: false
      schema:
        type: string
      description: Filter by theme/category, e.g. Planning
    get:
      tags:
      - gis
      summary: List cataloged GIS layers (gis.layers) offered to the project, with source provenance and
        current feature counts
      responses:
        default:
          description: OK
      operationId: getProjectsByProjectIdGisLayers
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: gis
        surface: platform
        role: viewer
        source: server/db/gis_catalog_service.list_catalog_layers
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/gis-layers
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/gis-layers/{layerCode}/features:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: layerCode
      in: path
      required: true
      schema:
        type: string
      description: Logical layer code, e.g. planning.urban_renewal_declared
    - name: bbox
      in: query
      required: false
      schema:
        type: string
      description: minLon,minLat,maxLon,maxLat in EPSG:4326; server-side filtered
    - name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 5000
        default: 2000
    get:
      tags:
      - gis
      summary: Current-snapshot features (valid_to IS NULL) of a cataloged GIS layer as EPSG:4326 GeoJSON,
        bbox-filtered and limited (read-only; authoritative layers)
      responses:
        default:
          description: OK
        '400':
          description: INVALID_BBOX or INVALID_LIMIT
      operationId: getProjectsByProjectIdGisLayersByLayerCodeFeatures
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: gis
        surface: platform
        role: viewer
        crs: EPSG:4326 output; bbox in EPSG:4326
        source: server/db/gis_catalog_service.query_layer_features
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/gis-layers/planning.urban_renewal_declared/features
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/gis-imports:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - gis
      summary: 'Import an official/authoritative layer (from a frontend upload) into the gis.* catalog:
        creates data_source/dataset/layer (read-only, theme e.g. Planning) and imports the posted GeoJSON
        FeatureCollection (body: {name, theme, geojson, sourceCrs, sourceUrl}). Reviewer/admin role.'
      responses:
        '201':
          description: Created
        '400':
          description: INVALID_ARGUMENT
      operationId: postProjectsByProjectIdGisImports
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: gis
        surface: platform
        role: reviewer
        bodyLimitMb: 32
        source: server/db/gis_catalog_service.create_and_import_layer
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/gis-imports
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/spatial-analysis/overlays:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
      description: 'GET: max overlay runs to list'
    get:
      tags:
      - spatial-analysis
      summary: List persisted spatial-overlay runs for the project (newest first), each with metrics,
        source references, and derived output layer codes
      responses:
        default:
          description: OK
      operationId: getProjectsByProjectIdSpatialAnalysisOverlays
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: spatial-analysis
        surface: platform
        role: viewer
        source: server/db/spatial_overlay_service.list_runs
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/spatial-analysis/overlays
        sourceDocument: server/db/openapi.yaml
    post:
      tags:
      - spatial-analysis
      summary: 'Run a governed polygon overlay (skill: skylens-spatial-overlay-analysis). Body: {spec:{operation,scope,mask,topology,outputs},
        outputMode:''preview''|''persist'', idempotencyKey}. Topology runs in PostGIS/EPSG:2039; persist
        materializes derived covered_by_mask/uncovered_by_mask gis.* layers. Preview needs viewer; persist
        needs reviewer+. Idempotent per (org,project,idempotencyKey).'
      responses:
        '201':
          description: Completed
        '400':
          description: INVALID_ARGUMENT
        '403':
          description: FORBIDDEN
        '404':
          description: NOT_FOUND (input layer or project)
        '429':
          description: QUOTA_EXCEEDED
      operationId: postProjectsByProjectIdSpatialAnalysisOverlays
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: spatial-analysis
        surface: platform
        role: viewer for outputMode=preview, reviewer for persist
        idempotency: body
        crs: topology runs in EPSG:2039
        source: server/db/spatial_overlay_service.run_overlay
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/spatial-analysis/overlays
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/spatial-analysis/overlays/{runId}:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: runId
      in: path
      required: true
      schema:
        type: string
    get:
      tags:
      - spatial-analysis
      summary: Get a spatial-overlay run manifest (status, operation, source versions, metrics, output
        layer IDs, lineage, warnings)
      responses:
        default:
          description: OK
        '404':
          description: NOT_FOUND
      operationId: getProjectsByProjectIdSpatialAnalysisOverlaysByRunId
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: spatial-analysis
        surface: platform
        role: viewer
        source: server/db/spatial_overlay_service.get_run
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/spatial-analysis/overlays/bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/layer-exports/{format}:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: format
      in: path
      required: true
      schema:
        type: string
        enum:
        - geojson
        - shapefile
        - csv
        - entities_json
      description: The four formats export_service.FORMATS actually serves. 'kml' was declared here at
        baseline and has never been implemented.
    - name: crs
      in: query
      required: false
      schema:
        type: string
        enum:
        - '4326'
        - project
        default: '4326'
      description: '''4326'' or the literal ''project'' (core.projects.default_crs_epsg, 2039 by default).
        Any other value is 400 — there is no on-the-fly reprojection to an arbitrary EPSG.'
    - name: scope
      in: query
      required: false
      schema:
        type: string
    - name: layerId
      in: query
      required: false
      schema:
        type: string
    - name: entityTypeId
      in: query
      required: false
      schema:
        type: string
    - name: reviewState
      in: query
      required: false
      schema:
        type: string
    - name: limit
      in: query
      required: false
      schema:
        type: integer
    - name: offset
      in: query
      required: false
      schema:
        type: integer
    get:
      tags:
      - export
      summary: 'Export approved features as a downloadable file (Content-Disposition attachment). NOTE:
        GET here vs POST .../layers/{id}/export on Firebase — reconcile.'
      responses:
        '200':
          description: File body
        '400':
          description: INVALID_ARGUMENT (unknown format or crs)
        '403':
          description: FORBIDDEN
      operationId: getProjectsByProjectIdLayerExportsByFormat
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: export
        surface: platform
        role: viewer
        crs: crs=4326 (default) or crs=project
        source: server/db/export_service.export_features
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/layer-exports/geojson
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/photo-collections:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
      description: AUTHORITATIVE tenant boundary for this request
    - name: publicationStatus
      in: query
      required: false
      schema:
        type: string
        enum:
        - draft
        - ready_for_review
        - published
        - rejected
        - archived
      description: 'GET: filter the listing'
    get:
      tags:
      - photos
      summary: List the project's Photo Collections (viewer role). The Viewer layer requests publicationStatus=published.
      responses:
        default:
          description: OK
        '403':
          description: FORBIDDEN
      operationId: getProjectsByProjectIdPhotoCollections
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: photos
        surface: platform
        role: viewer
        source: server/db/photo_import_service.list_collections
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/photo-collections
        sourceDocument: server/db/openapi.yaml
    post:
      tags:
      - photos
      summary: Register an immutable Photo Collection version + its assets and return a deterministic,
        server-derived upload plan (editor role). Idempotent per (projectId, collectionKey, version) with
        an identical manifest digest; a different manifest for the same version is INVALID_STATE — corrected
        inventories are NEW versions. The API never proxies image bytes.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - name
              - collectionKey
              - manifest
              properties:
                name:
                  type: string
                collectionKey:
                  type: string
                  description: Sanitized server-side to [A-Za-z0-9_-]{1,120}
                version:
                  type: integer
                  minimum: 1
                  default: 1
                manifest:
                  type: object
                  required:
                  - files
                  description: Client preflight inventory. Paths are validated server-side (no absolute
                    paths, no traversal, no case collisions, no __skylens/ prefix).
                  properties:
                    files:
                      type: array
                      items:
                        type: object
                organizationId:
                  type: string
                  format: uuid
                  description: CROSS-CHECK ONLY. Must equal core.projects.organization_id for the projectId
                    in the URL, which is verified under FOR SHARE inside the write transaction. A mismatch
                    is 403 FORBIDDEN. Omitting it skips the cross-check; it never selects a tenant.
                panoramaDeclaration:
                  type: object
                  description: Intent recorded on the collection, NOT a registration — panorama groups
                    are created later by /panoramas, after the stitched bytes exist. Manual placement
                    is normalized server-side to positionSource=manual, spatialConfidence=approximate
                    and cannot be promoted.
                  properties:
                    assetMode:
                      type: string
                      enum:
                      - mixed
                      - panorama
                      - source_frames
                      default: mixed
                    confirmedEquirectangular:
                      type: boolean
                      description: Permits immersive viewing only. NOT a calibration.
                    placement:
                      type: object
                      nullable: true
                      properties:
                        center:
                          type: array
                          items:
                            type: number
                          minItems: 2
                          maxItems: 3
                          description: '[lon, lat, heightM?] in EPSG:4326'
                        headingDeg:
                          type: number
                          minimum: 0
                          maximum: 360
                          nullable: true
                          description: Typed by the user -> orientationSource=manual, never calibrated
                publicationRequested:
                  type: boolean
                  default: false
                  description: Intent flag only. Publication still requires the separate REVIEWER-role
                    /publish call.
                captureStartedAt:
                  type: string
                  format: date-time
                  nullable: true
                captureEndedAt:
                  type: string
                  format: date-time
                  nullable: true
                storageBucket:
                  type: string
                  nullable: true
                platform:
                  type: string
                  nullable: true
                camera:
                  type: string
                  nullable: true
                operator:
                  type: string
                  nullable: true
                mission:
                  type: string
                  nullable: true
                sourceCrs:
                  type: string
                  nullable: true
                heightDatum:
                  type: string
                  nullable: true
                privacy:
                  type: object
                  nullable: true
                provenance:
                  type: object
                  nullable: true
                metadata:
                  type: object
                  nullable: true
      responses:
        '201':
          description: Registered; body carries the collection and its uploadPlan (server-derived keys)
        '400':
          description: INVALID_ARGUMENT
        '403':
          description: FORBIDDEN — actor not authorized for the project, or organizationId does not own
            it
        '404':
          description: NOT_FOUND — the project does not exist
        '409':
          description: INVALID_STATE — same version, different manifest digest
        '422':
          description: VALIDATION_FAILED
      operationId: postProjectsByProjectIdPhotoCollections
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: photos
        surface: platform
        role: editor
        bodyLimitMb: 32
        idempotency: per (projectId, collectionKey, version) + manifest digest
        source: server/db/photo_import_service.create_collection
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/photo-collections
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/photo-collections/{collectionId}:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
      description: AUTHORITATIVE; the collection lookup is filtered by it, so cross-project access is
        404
    - name: collectionId
      in: path
      required: true
      schema:
        type: string
    get:
      tags:
      - photos
      summary: 'Collection detail: asset counts, camera-solution versions, and panorama groups with projection,
        center, positionSource, headingDeg, orientationSource and spatialConfidence (viewer role)'
      responses:
        default:
          description: OK
        '404':
          description: NOT_FOUND
      operationId: getProjectsByProjectIdPhotoCollectionsByCollectionId
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: photos
        surface: platform
        role: viewer
        source: server/db/photo_import_service.get_collection
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/photo-collections/88888888-8888-4888-8888-888888888888
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/photo-collections/{collectionId}/images:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: collectionId
      in: path
      required: true
      schema:
        type: string
    - name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 1000
        default: 200
    - name: offset
      in: query
      required: false
      schema:
        type: integer
        minimum: 0
        default: 0
    - name: role
      in: query
      required: false
      schema:
        type: string
        enum:
        - original_still
        - panorama_source_frame
        - stitched_panorama
        - solution_artifact
        - support_document
        - derivative
    get:
      tags:
      - photos
      summary: Paged asset listing for the Viewer layer (viewer role) — delivery references and GNSS/solved
        positions, never bytes. Solved poses are returned displayOnly=true and never drive picking.
      responses:
        default:
          description: OK
        '403':
          description: FORBIDDEN
        '404':
          description: NOT_FOUND — including a collection that belongs to another project
      operationId: getProjectsByProjectIdPhotoCollectionsByCollectionIdImages
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: photos
        surface: platform
        role: viewer
        paginated: offset
        source: server/db/photo_import_service.list_images
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/photo-collections/88888888-8888-4888-8888-888888888888/images
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/photo-collections/{collectionId}/complete-upload:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: collectionId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - photos
      summary: Mark direct-to-storage uploads done (editor role). Body {uploaded:[{path}], s3BaseUrl?}.
        Object URIs are rebuilt from the REGISTERED asset role and the stored prefix — a client cannot
        relocate an asset outside its collection.
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_ARGUMENT
        '403':
          description: FORBIDDEN
        '409':
          description: INVALID_STATE
      operationId: postProjectsByProjectIdPhotoCollectionsByCollectionIdCompleteUpload
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: photos
        surface: platform
        role: editor
        source: server/db/photo_import_service.complete_upload
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/photo-collections/88888888-8888-4888-8888-888888888888/complete-upload
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/photo-collections/{collectionId}/validate:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: collectionId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - photos
      summary: Re-validate stored bytes from object storage (signature, size, SHA-256, dimensions). Degrades
        to a documented note when the server has no storage credentials; the collection then keeps client-preflight
        evidence only.
      responses:
        '200':
          description: OK
        '403':
          description: FORBIDDEN
      operationId: postProjectsByProjectIdPhotoCollectionsByCollectionIdValidate
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: photos
        surface: platform
        role: editor
        source: server/db/photo_validation_service.validate_collection_bytes
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/photo-collections/88888888-8888-4888-8888-888888888888/validate
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/photo-collections/{collectionId}/solutions:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: collectionId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - photos
      summary: Attach one immutable Bentley BlocksExchange/ATExport camera-solution version (editor role).
        Body {filename, contentBase64, producer?, producerVersion?}. Lands as 'parsed' or 'needs_review'
        — never auto-validated.
      responses:
        '201':
          description: Created
        '400':
          description: INVALID_ARGUMENT
        '422':
          description: VALIDATION_FAILED
      operationId: postProjectsByProjectIdPhotoCollectionsByCollectionIdSolutions
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: photos
        surface: platform
        role: editor
        source: server/db/photo_import_service.attach_solution
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/photo-collections/88888888-8888-4888-8888-888888888888/solutions
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/photo-collections/{collectionId}/panoramas:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: collectionId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - photos
      summary: 'Register reviewed panorama groups (editor role), after the stitched bytes exist and validate.
        projection defaults to ''unknown'': a 2:1 ratio is a candidate, not proof, and the client sends
        ''equirectangular'' only from GPano metadata or explicit per-image user confirmation.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - groups
              properties:
                groups:
                  type: array
                  items:
                    type: object
                    properties:
                      name:
                        type: string
                        nullable: true
                      stitchedPath:
                        type: string
                        nullable: true
                        description: Must already be in this collection's inventory
                      sourcePaths:
                        type: array
                        items:
                          type: string
                      projection:
                        type: string
                        enum:
                        - equirectangular
                        - partial_sphere
                        - unknown
                        default: unknown
                      widthPx:
                        type: integer
                        nullable: true
                      heightPx:
                        type: integer
                        nullable: true
                      center:
                        type: array
                        items:
                          type: number
                        minItems: 2
                        maxItems: 3
                        nullable: true
                        description: '[lon, lat, heightM?] EPSG:4326'
                      positionSource:
                        type: string
                        enum:
                        - solution
                        - gnss
                        - manual
                        - none
                      headingDeg:
                        type: number
                        minimum: 0
                        maximum: 360
                        nullable: true
                      orientationSource:
                        type: string
                        enum:
                        - metadata
                        - manual
                        - unknown
                        description: '''solution'' is REJECTED at import — a solved orientation is claimed
                          by a solution version and confirmed in review. ''manual'' requires a headingDeg
                          and is never calibrated.'
                      spatialConfidence:
                        type: string
                        enum:
                        - approximate
                        - unplaced
                        description: '''validated'' is REJECTED at import — it is set by review only,
                          and it is the sole gate for metric raycast, entity projection and Cesium camera
                          synchronization. positionSource=manual is forced to ''approximate''.'
      responses:
        '201':
          description: Created; body carries panoramaGroupIds and count
        '400':
          description: INVALID_ARGUMENT — including an import-time claim of 'validated' or orientationSource
            'solution'
        '403':
          description: FORBIDDEN
        '422':
          description: VALIDATION_FAILED — stitchedPath/sourcePath not in the collection
      operationId: postProjectsByProjectIdPhotoCollectionsByCollectionIdPanoramas
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: photos
        surface: platform
        role: editor
        crs: center is [lon, lat, heightM?] EPSG:4326
        source: server/db/photo_import_service.register_panoramas
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/photo-collections/88888888-8888-4888-8888-888888888888/panoramas
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/photo-collections/{collectionId}/panorama-height:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: collectionId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - photos
      summary: Correct the CAMERA HEIGHT of the collection's placed panorama groups (editor role). A reviewed
        height is mutable even though the image inventory is not — map picking returns the surface under
        the cursor, not the altitude the camera flew at. Longitude/latitude are untouched (a different
        position is a re-placement, not a height correction), and positionSource / orientationSource /
        spatialConfidence are unchanged, so editing a height can never promote a panorama toward 'validated'.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - heightM
              properties:
                heightM:
                  type: number
                  minimum: -500
                  maximum: 10000
                  description: Metres. Bounds span the Dead Sea shore to above any panorama-capable camera.
                panoramaGroupId:
                  type: string
                  format: uuid
                  description: Optional. Omit to correct every PLACED group in the collection.
      responses:
        '200':
          description: Updated; body carries {updated:[{panoramaGroupId, center}], count, heightM}
        '400':
          description: INVALID_ARGUMENT — non-numeric or out-of-range height
        '403':
          description: FORBIDDEN
        '404':
          description: NOT_FOUND — collection is not in this project
        '409':
          description: INVALID_STATE — no placed panorama matched (an unplaced panorama has no height
            to correct)
      operationId: postProjectsByProjectIdPhotoCollectionsByCollectionIdPanoramaHeight
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: photos
        surface: platform
        role: editor
        source: server/db/photo_import_service.set_panorama_height
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/photo-collections/88888888-8888-4888-8888-888888888888/panorama-height
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/photo-collections/{collectionId}/publish:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: collectionId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - photos
      summary: Publish the collection to the Viewer (REVIEWER role). Upload completion is NOT publication
        — this is the separate authorized action. Bounds derive from GNSS points, normalized poses and
        panorama centers; publication never mutates assets.
      responses:
        '200':
          description: OK
        '403':
          description: FORBIDDEN — needs the reviewer role
        '409':
          description: INVALID_STATE
      operationId: postProjectsByProjectIdPhotoCollectionsByCollectionIdPublish
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: photos
        surface: platform
        role: reviewer
        source: server/db/photo_import_service.publish_collection
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/photo-collections/88888888-8888-4888-8888-888888888888/publish
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/photo-collections/{collectionId}/cancel:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: collectionId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - photos
      summary: Abandon an unpublished import (editor role). Published collections are immutable — archive
        instead.
      responses:
        '200':
          description: OK
        '409':
          description: INVALID_STATE
      operationId: postProjectsByProjectIdPhotoCollectionsByCollectionIdCancel
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: photos
        surface: platform
        role: editor
        source: server/db/photo_import_service.cancel_collection
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/photo-collections/88888888-8888-4888-8888-888888888888/cancel
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/photo-assets/{assetId}/delivery:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: assetId
      in: path
      required: true
      schema:
        type: string
    - name: ttlSeconds
      in: query
      required: false
      schema:
        type: integer
    get:
      tags:
      - photos
      summary: Short-lived authorized delivery URL for one asset (viewer role). Presigned when the server
        holds storage credentials; falls back to the stored https URI otherwise. Originals stay private
        — this is the ONLY read path the Viewer may use.
      responses:
        default:
          description: OK
        '403':
          description: FORBIDDEN
        '404':
          description: NOT_FOUND
      operationId: getProjectsByProjectIdPhotoAssetsByAssetIdDelivery
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: photos
        surface: platform
        role: viewer
        source: server/db/photo_validation_service.get_asset_delivery
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/photo-assets/99999999-9999-4999-8999-999999999999/delivery
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/ai/dreams:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: status
      in: query
      required: false
      schema:
        type: string
        enum:
        - queued
        - running
        - completed
        - failed
        - cancelled
      description: 'GET: filter by run status'
    - name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    - name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        minLength: 8
        maxLength: 200
      description: 'POST: replay-safe key; a body idempotencyKey is also accepted, header wins'
    get:
      tags:
      - agent-memory
      summary: List memory-consolidation (dream) runs, newest first. Requires agent_memory.read
      responses:
        default:
          description: OK
      operationId: getProjectsByProjectIdAiDreams
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: agent-memory
        surface: platform
        permission: agent_memory.read
        flag: SKYLENS_AGENT_DREAMS_ENABLED
        source: server/db/agent_dream_service.list_dreams
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/ai/dreams
        sourceDocument: server/db/openapi.yaml
    post:
      tags:
      - agent-memory
      summary: Queue a dream run. Body {agentKey, scope, scopeRef, selection:{startedAfter,startedBefore,outcomeStatuses,minOutcomeQuality,sessionIds},
        instructions, adapterKey}. Selects sessions at request time and freezes the manifest. Produces
        a CANDIDATE store only — it cannot change what any agent reads. Requires agent_memory.run
      responses:
        '201':
          description: Queued
        '400':
          description: INVALID_ARGUMENT
        '403':
          description: FORBIDDEN
        '409':
          description: IDEMPOTENCY_CONFLICT | INVALID_STATE | FEATURE_DISABLED
      operationId: postProjectsByProjectIdAiDreams
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: agent-memory
        surface: platform
        permission: agent_memory.run
        flag: SKYLENS_AGENT_DREAMS_ENABLED
        idempotency: header or body
        source: server/db/agent_dream_service.create_dream
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/ai/dreams
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/ai/dreams/{dreamId}:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: dreamId
      in: path
      required: true
      schema:
        type: string
    get:
      tags:
      - agent-memory
      summary: Get one dream run with its session manifest, findings (each with supporting sessions/items/evidence)
        and evaluation. Requires agent_memory.read
      responses:
        default:
          description: OK
        '404':
          description: NOT_FOUND
      operationId: getProjectsByProjectIdAiDreamsByDreamId
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: agent-memory
        surface: platform
        permission: agent_memory.read
        source: server/db/agent_dream_service.get_dream
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/ai/dreams/dddddddd-dddd-4ddd-8ddd-dddddddddddd
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/ai/dreams/{dreamId}/diff:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: dreamId
      in: path
      required: true
      schema:
        type: string
    get:
      tags:
      - agent-memory
      summary: Structured baseline-vs-candidate diff bucketed as added/merged/modified/superseded/removed/unresolved.
        Requires agent_memory.read
      responses:
        default:
          description: OK
        '404':
          description: NOT_FOUND
        '409':
          description: INVALID_STATE (no candidate yet)
      operationId: getProjectsByProjectIdAiDreamsByDreamIdDiff
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: agent-memory
        surface: platform
        permission: agent_memory.read
        source: server/db/agent_dream_service.get_diff
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/ai/dreams/dddddddd-dddd-4ddd-8ddd-dddddddddddd/diff
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/ai/dreams/{dreamId}/cancel:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: dreamId
      in: path
      required: true
      schema:
        type: string
    - name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
    post:
      tags:
      - agent-memory
      summary: Cancel a non-terminal run. The active binding is never touched. Requires agent_memory.run
      responses:
        default:
          description: OK
        '409':
          description: INVALID_STATE
      operationId: postProjectsByProjectIdAiDreamsByDreamIdCancel
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: agent-memory
        surface: platform
        permission: agent_memory.run
        idempotency: header or body
        source: server/db/agent_dream_service.cancel_dream
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/ai/dreams/dddddddd-dddd-4ddd-8ddd-dddddddddddd/cancel
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/ai/dreams/{dreamId}/approve:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: dreamId
      in: path
      required: true
      schema:
        type: string
    - name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
    post:
      tags:
      - agent-memory
      summary: Record an APPROVAL of the candidate. Does NOT activate it — activation is a separate call
        requiring agent_memory.activate. Requires agent_memory.review
      responses:
        default:
          description: OK
        '403':
          description: FORBIDDEN
        '409':
          description: INVALID_STATE
      operationId: postProjectsByProjectIdAiDreamsByDreamIdApprove
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: agent-memory
        surface: platform
        permission: agent_memory.review
        idempotency: header or body
        source: server/db/agent_dream_service.review_dream
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/ai/dreams/dddddddd-dddd-4ddd-8ddd-dddddddddddd/approve
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/ai/dreams/{dreamId}/reject:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: dreamId
      in: path
      required: true
      schema:
        type: string
    - name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
    post:
      tags:
      - agent-memory
      summary: Reject the candidate. Body {reason} is REQUIRED and is stored in the audit trail. Requires
        agent_memory.review
      responses:
        default:
          description: OK
        '400':
          description: INVALID_ARGUMENT (missing reason)
      operationId: postProjectsByProjectIdAiDreamsByDreamIdReject
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: agent-memory
        surface: platform
        permission: agent_memory.review
        idempotency: header or body
        source: server/db/agent_dream_service.review_dream
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/ai/dreams/dddddddd-dddd-4ddd-8ddd-dddddddddddd/reject
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/ai/memory-stores:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: agentKey
      in: query
      required: false
      schema:
        type: string
    - name: status
      in: query
      required: false
      schema:
        type: string
        enum:
        - candidate
        - active
        - rejected
        - archived
    - name: active
      in: query
      required: false
      schema:
        type: boolean
      description: true returns {activeStore} — the currently bound store plus its items and binding version
    - name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    get:
      tags:
      - agent-memory
      summary: List memory stores, or fetch the active one. Requires agent_memory.read
      responses:
        default:
          description: OK
      operationId: getProjectsByProjectIdAiMemoryStores
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: agent-memory
        surface: platform
        permission: agent_memory.read
        source: server/db/agent_memory_service.list_stores
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/ai/memory-stores
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/ai/memory-stores/{storeId}:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: storeId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    get:
      tags:
      - agent-memory
      summary: Get one memory store with its typed items, trust labels, confidence, provenance and unresolved
        flags. Requires agent_memory.read
      responses:
        default:
          description: OK
        '404':
          description: NOT_FOUND
      operationId: getProjectsByProjectIdAiMemoryStoresByStoreId
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: agent-memory
        surface: platform
        permission: agent_memory.read
        source: server/db/agent_memory_service.get_store
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/ai/memory-stores/cccccccc-cccc-4ccc-8ccc-cccccccccccc
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/ai/memory-stores/{storeId}/activate:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: storeId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    post:
      tags:
      - agent-memory
      summary: 'Atomically make this store active. Body {expectedBindingVersion, reason}. Compare-and-swap:
        expectedBindingVersion is REQUIRED once a binding exists, and a stale value returns REVISION_CONFLICT
        so two candidates from one baseline cannot both activate. Requires agent_memory.activate'
      responses:
        default:
          description: OK
        '403':
          description: FORBIDDEN
        '409':
          description: REVISION_CONFLICT | INVALID_STATE
      operationId: postProjectsByProjectIdAiMemoryStoresByStoreIdActivate
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: agent-memory
        surface: platform
        permission: agent_memory.activate
        concurrency: expectedBindingVersion
        source: server/db/agent_memory_service.activate_store
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/ai/memory-stores/cccccccc-cccc-4ccc-8ccc-cccccccccccc/activate
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/ai/memory-stores/{storeId}/rollback:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: storeId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    post:
      tags:
      - agent-memory
      summary: Restore a previously-active store. Same compare-and-swap semantics as activate; binding_version
        is monotonic (a rollback increments it). A rejected store can never be rolled back to. Requires
        agent_memory.activate
      responses:
        default:
          description: OK
        '409':
          description: REVISION_CONFLICT | INVALID_STATE
      operationId: postProjectsByProjectIdAiMemoryStoresByStoreIdRollback
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: agent-memory
        surface: platform
        permission: agent_memory.activate
        concurrency: expectedBindingVersion
        source: server/db/agent_memory_service.rollback_binding
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/ai/memory-stores/cccccccc-cccc-4ccc-8ccc-cccccccccccc/rollback
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/ai/agent-sessions:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: agentKey
      in: query
      required: false
      schema:
        type: string
    - name: startedAfter
      in: query
      required: false
      schema:
        type: string
        format: date-time
    - name: startedBefore
      in: query
      required: false
      schema:
        type: string
        format: date-time
    - name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 25
    get:
      tags:
      - agent-memory
      summary: List recorded agent sessions eligible for consolidation. Requires agent_memory.read
      responses:
        default:
          description: OK
      operationId: getProjectsByProjectIdAiAgentSessions
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: agent-memory
        surface: platform
        permission: agent_memory.read
        source: server/db/agent_memory_service.list_sessions
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/ai/agent-sessions
        sourceDocument: server/db/openapi.yaml
    post:
      tags:
      - agent-memory
      summary: Record a completed session. Body {session:{sessionId,agentKey,startedAt,endedAt,outcomeStatus,toolCalls,errorSummary,transcriptUri,contentHash,metadata}}.
        Transcript BODIES are rejected — pass a transcriptUri into object storage plus its hash. Requires
        agent_memory.run
      responses:
        '201':
          description: Recorded
        '400':
          description: INVALID_ARGUMENT (inline transcript, missing startedAt)
        '409':
          description: DUPLICATE_REQUEST
      operationId: postProjectsByProjectIdAiAgentSessions
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: agent-memory
        surface: platform
        permission: agent_memory.run
        source: server/db/agent_memory_service.record_session
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/ai/agent-sessions
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/ai/agent-context:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - agent-memory
      summary: Assemble the doc-14 Context Package for one agent turn. Body {intent, agentKey, scope,
        scopeRef, focus, focusTypes, memoryTokenBudget}. Reads the ACTIVE store only; operational memory
        is emitted at priority 8 (lowest) and can never override an authoritative spatial source. Requires
        agent_memory.read
      responses:
        default:
          description: OK
      operationId: postProjectsByProjectIdAiAgentContext
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: agent-memory
        surface: platform
        permission: agent_memory.read
        source: server/db/agent_context_service.assemble_context
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/ai/agent-context
        sourceDocument: server/db/openapi.yaml
  /api/projects:
    get:
      tags:
      - projects
      summary: List the projects the verified principal can access, newest-updated first.
      parameters:
      - name: organizationId
        in: query
        required: false
        schema:
          type: string
          format: uuid
      - name: status
        in: query
        required: false
        schema:
          type: string
          enum:
          - draft
          - active
          - on_hold
          - completed
          - archived
      - name: projectType
        in: query
        required: false
        schema:
          type: string
      - name: search
        in: query
        required: false
        schema:
          type: string
      - name: cursor
        in: query
        required: false
        schema:
          type: string
        description: Opaque keyset cursor from the previous page's nextCursor.
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 100
          default: 25
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectPage'
        '400':
          description: INVALID_ARGUMENT
        '401':
          description: UNAUTHENTICATED
      operationId: getProjects
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: projects
        surface: platform
        permission: projects.read
        paginated: cursor
        source: server/db/extraction_api.py (PROJECTS_COLLECTION_ROUTE)
        examplePath: /api/projects
        sourceDocument: server/db/openapi.yaml
    post:
      tags:
      - projects
      summary: 'Create a project inside an organization. Requires projects.create, which only organization_admin
        and above hold. Idempotent per idempotencyKey: a replay returns 200 with the existing project
        instead of 201.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - organizationId
              - name
              properties:
                organizationId:
                  type: string
                  format: uuid
                name:
                  type: string
                slug:
                  type: string
                  nullable: true
                projectType:
                  type: string
                  nullable: true
                description:
                  type: string
                  nullable: true
                accessMode:
                  type: string
                  default: organization
                defaultCrsEpsg:
                  type: integer
                  default: 2039
                  description: The project's storage/working CRS. Defaults to EPSG:2039 (Israel TM Grid);
                    geometry is stored in it and EPSG:4326 is derived.
                idempotencyKey:
                  type: string
                  nullable: true
                metadata:
                  type: object
                  nullable: true
      responses:
        '201':
          description: Created
        '200':
          description: Idempotent replay — the project already existed
        '400':
          description: INVALID_ARGUMENT
        '403':
          description: FORBIDDEN — needs projects.create
      operationId: postProjects
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: projects
        surface: platform
        permission: projects.create
        idempotency: body
        source: server/db/extraction_api.py (PROJECTS_COLLECTION_ROUTE)
        examplePath: /api/projects
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    get:
      tags:
      - projects
      summary: Project detail plus its content inventory (reality models, photo collections, approved
        and planning layer counts).
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Project'
        '403':
          description: FORBIDDEN
        '404':
          description: NOT_FOUND
      operationId: getProjectsByProjectId
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: projects
        surface: platform
        permission: projects.read
        source: server/db/extraction_api.py (PROJECT_DETAIL_ROUTE)
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111
        sourceDocument: server/db/openapi.yaml
    patch:
      tags:
      - projects
      summary: Update mutable project fields. Requires projects.update (project_admin+). Unknown fields
        are REJECTED, never ignored.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
                projectType:
                  type: string
                status:
                  type: string
                  enum:
                  - draft
                  - active
                  - on_hold
                  - completed
                  - archived
                accessMode:
                  type: string
      responses:
        '200':
          description: OK
        '400':
          description: UNKNOWN_FIELD / INVALID_ARGUMENT
        '403':
          description: FORBIDDEN
      operationId: patchProjectsByProjectId
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: projects
        surface: platform
        permission: projects.update
        source: server/db/extraction_api.py (PROJECT_DETAIL_ROUTE)
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/archive:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    post:
      tags:
      - projects
      summary: Archive a project (projects.archive, project_admin+). Body takes no fields.
      responses:
        '200':
          description: OK
        '403':
          description: FORBIDDEN
      operationId: postProjectsByProjectIdArchive
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: projects
        surface: platform
        permission: projects.archive
        source: server/db/extraction_api.py (PROJECT_ARCHIVE_ROUTE)
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/archive
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/resources:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    - name: kind
      in: query
      required: false
      schema:
        type: string
      description: 'GET: filter the listing by resource kind.'
    get:
      tags:
      - documents
      summary: List the project's documents and attachments (metadata only — never bytes). Requires resources.read.
      responses:
        '200':
          description: OK
        '403':
          description: FORBIDDEN
      operationId: getProjectsByProjectIdResources
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: documents
        surface: platform
        permission: resources.read
        source: server/db/extraction_api.py (PROJECT_RESOURCES_ROUTE)
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/resources
        sourceDocument: server/db/openapi.yaml
    post:
      tags:
      - documents
      summary: Attach a document to the project (resources.upload, editor+). Either contentBase64 (inline
        bytes, up to the 32MB body limit) or sourceUrl.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - name
              properties:
                name:
                  type: string
                kind:
                  type: string
                  default: document
                description:
                  type: string
                  nullable: true
                contentType:
                  type: string
                  nullable: true
                contentBase64:
                  type: string
                  nullable: true
                  description: Base64 bytes. The request body limit for this route is 32MB.
                sourceUrl:
                  type: string
                  nullable: true
                metadata:
                  type: object
                  nullable: true
      responses:
        '201':
          description: Created
        '400':
          description: INVALID_ARGUMENT
        '403':
          description: FORBIDDEN
        '413':
          description: PAYLOAD_TOO_LARGE
      operationId: postProjectsByProjectIdResources
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: documents
        surface: platform
        permission: resources.upload
        bodyLimitMb: 32
        source: server/db/extraction_api.py (PROJECT_RESOURCES_ROUTE)
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/resources
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/resources/{resourceId}:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    - name: resourceId
      in: path
      required: true
      schema:
        type: string
    - name: download
      in: query
      required: false
      schema:
        type: string
        enum:
        - '1'
        - 'true'
        - 'yes'
      description: 'GET: return the stored bytes as well as the metadata.'
    get:
      tags:
      - documents
      summary: Get one document's metadata, or its bytes with ?download=1. Requires resources.read.
      responses:
        '200':
          description: OK
        '403':
          description: FORBIDDEN
        '404':
          description: NOT_FOUND
      operationId: getProjectsByProjectIdResourcesByResourceId
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: documents
        surface: platform
        permission: resources.read
        source: server/db/extraction_api.py (PROJECT_RESOURCE_ROUTE)
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/resources/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa
        sourceDocument: server/db/openapi.yaml
    delete:
      tags:
      - documents
      summary: Delete a document. Requires resources.delete — held by human editors and above, and deliberately
        NOT by the ai_worker service principal.
      responses:
        '200':
          description: OK
        '403':
          description: FORBIDDEN
      operationId: deleteProjectsByProjectIdResourcesByResourceId
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: documents
        surface: platform
        permission: resources.delete
        source: server/db/extraction_api.py (PROJECT_RESOURCE_ROUTE)
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/resources/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/members:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    get:
      tags:
      - projects
      summary: List project members and their roles. Requires members.manage (project_admin+).
      responses:
        '200':
          description: OK
        '403':
          description: FORBIDDEN
      operationId: getProjectsByProjectIdMembers
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: projects
        surface: platform
        permission: members.manage
        source: server/db/extraction_api.py (PROJECT_MEMBERS_ROUTE)
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/members
        sourceDocument: server/db/openapi.yaml
    post:
      tags:
      - projects
      summary: Add a member. Requires members.manage AND the Idempotency-Key header. A caller may never
        grant a role that outranks their own (409 ROLE_RANK_VIOLATION). Returns 200 rather than 201 on
        an idempotent replay.
      parameters:
      - $ref: '#/components/parameters/IdempotencyKeyRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - userId
              - role
              properties:
                userId:
                  type: string
                  format: uuid
                role:
                  type: string
                  enum:
                  - viewer
                  - editor
                  - reviewer
                  - project_admin
                  - organization_admin
                  - ai_worker
                  - integration_service
      responses:
        '201':
          description: Created
        '200':
          description: Idempotent replay
        '400':
          description: IDEMPOTENCY_KEY_REQUIRED / INVALID_MEMBER_ROLE
        '403':
          description: FORBIDDEN / ROLE_RANK_VIOLATION
        '409':
          description: MEMBER_ALREADY_EXISTS
      operationId: postProjectsByProjectIdMembers
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: projects
        surface: platform
        permission: members.manage
        idempotency: header
        source: server/db/extraction_api.py (PROJECT_MEMBERS_ROUTE)
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/members
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/members/{userId}:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    - name: userId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    - $ref: '#/components/parameters/IdempotencyKeyRequired'
    patch:
      tags:
      - projects
      summary: Change a member's role. Body {role, expectedRole?} — expectedRole makes the change a compare-and-swap
        (409 VERSION_CONFLICT when stale).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - role
              properties:
                role:
                  type: string
                expectedRole:
                  type: string
                  nullable: true
      responses:
        '200':
          description: OK
        '403':
          description: FORBIDDEN / ROLE_RANK_VIOLATION
        '404':
          description: MEMBER_NOT_FOUND
        '409':
          description: VERSION_CONFLICT | LAST_PROJECT_ADMIN
      operationId: patchProjectsByProjectIdMembersByUserId
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: projects
        surface: platform
        permission: members.manage
        idempotency: header
        source: server/db/extraction_api.py (PROJECT_MEMBER_ROUTE)
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/members/33333333-3333-4333-8333-333333333333
        sourceDocument: server/db/openapi.yaml
    delete:
      tags:
      - projects
      summary: Remove a member. Removing the last project_admin is refused (409 LAST_PROJECT_ADMIN).
      responses:
        '200':
          description: OK
        '403':
          description: FORBIDDEN
        '404':
          description: MEMBER_NOT_FOUND
        '409':
          description: LAST_PROJECT_ADMIN
      operationId: deleteProjectsByProjectIdMembersByUserId
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: projects
        surface: platform
        permission: members.manage
        idempotency: header
        source: server/db/extraction_api.py (PROJECT_MEMBER_ROUTE)
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/members/33333333-3333-4333-8333-333333333333
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/gis-layers/{layerCode}/search:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: layerCode
      in: path
      required: true
      schema:
        type: string
    - name: q
      in: query
      required: false
      schema:
        type: string
      description: Attribute search text. An empty q returns the layer's leading rows.
    - name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
    get:
      tags:
      - gis
      summary: Attribute search within one cataloged GIS layer's current snapshot. Read-only.
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_LIMIT
        '404':
          description: NOT_FOUND
      operationId: getProjectsByProjectIdGisLayersByLayerCodeSearch
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: gis
        surface: platform
        role: viewer
        source: server/db/gis_catalog_service.search_layer_features
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/gis-layers/planning.urban_renewal_declared/search
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/models:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: status
      in: query
      required: false
      schema:
        type: string
      description: 'GET: filter by model status.'
    get:
      tags:
      - reality-models
      summary: 'List the project''s Reality Models in CLIENT projection. storagePrefix, bucket and rootKey
        are never returned: where a model''s bytes live is the exact scope a signed CloudFront policy
        authorizes, so it stays server-side. Ask GET .../reality-models/{modelId}/delivery for an address
        instead. When SKYLENS_RASTER_PRODUCTS_ENABLED is on, orthophoto and terrain rows are served from
        reality.raster_products and are hidden from this listing.'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ModelList'
        '403':
          description: FORBIDDEN
      operationId: getProjectsByProjectIdModels
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: reality-models
        surface: platform
        role: viewer
        source: server/db/reality_model_service.list_models
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/models
        sourceDocument: server/db/openapi.yaml
    post:
      tags:
      - reality-models
      summary: Register an immutable Reality Model version (editor+). storagePrefix and rootKey are ABSENT
        from the accepted field set on purpose — the storage identity is derived server-side from (organization,
        project, model), so a caller that sends one gets 400 UNKNOWN_FIELD rather than a silently ignored
        field it believes took effect. rootFilename is the single path component a caller may still name,
        and it is validated to resolve inside the model's own prefix.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - datasetKey
              - name
              - tilesetUrl
              properties:
                datasetKey:
                  type: string
                name:
                  type: string
                tilesetUrl:
                  type: string
                modelType:
                  type: string
                  default: 3d_tiles
                  description: 3d_tiles | orthophoto | terrain | mesh — see the reality model architecture
                    doc.
                version:
                  type: integer
                  default: 1
                status:
                  type: string
                  default: available
                storageBucket:
                  type: string
                  nullable: true
                rootFilename:
                  type: string
                  nullable: true
                sourceCrsEpsg:
                  type: integer
                  nullable: true
                captureDate:
                  type: string
                  nullable: true
                producer:
                  type: string
                  nullable: true
                fileCount:
                  type: integer
                  nullable: true
                totalBytes:
                  type: integer
                  nullable: true
                bounds:
                  type: object
                  nullable: true
                validation:
                  type: object
                  nullable: true
                provenance:
                  type: object
                  nullable: true
                metadata:
                  type: object
                  nullable: true
      responses:
        '201':
          description: Created
        '400':
          description: UNKNOWN_FIELD (storagePrefix/rootKey) / INVALID_ARGUMENT
        '403':
          description: FORBIDDEN
      operationId: postProjectsByProjectIdModels
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: reality-models
        surface: platform
        role: editor
        source: server/db/reality_model_service.register_model
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/models
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/viewer/source-layers:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: types
      in: query
      required: false
      schema:
        type: string
      description: Comma-separated source-layer types.
    - name: status
      in: query
      required: false
      schema:
        type: string
    - name: published
      in: query
      required: false
      schema:
        type: boolean
        default: true
    - name: cursor
      in: query
      required: false
      schema:
        type: string
    - name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
    get:
      tags:
      - viewer
      summary: Dynamic Viewer source-layer selector projected over the canonical reality catalog. Flag-gated
        by SKYLENS_API_VIEWER_SOURCE_LAYERS_ENABLED (default OFF -> 404) and optionally narrowed by a
        project allowlist.
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_ARGUMENT
        '404':
          description: NOT_FOUND (flag off or project not in the rollout allowlist)
      operationId: getProjectsByProjectIdViewerSourceLayers
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: viewer
        surface: platform
        role: viewer
        paginated: cursor
        flag: SKYLENS_API_VIEWER_SOURCE_LAYERS_ENABLED
        source: server/db/reality_source_layer_service.list_source_layers
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/viewer/source-layers
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/elevation/sample:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - elevation
      summary: Sample the project's DSM over an AOI and return height statistics (ground estimate, percentiles,
        nDSM range).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - bbox
              properties:
                bbox:
                  type: array
                  items:
                    type: number
                  minItems: 4
                  maxItems: 4
                  description: '[minX, minY, maxX, maxY] in bboxEpsg. Axis order is X/easting/longitude
                    first — NOT the EPSG-authority lat/lon order.'
                bboxEpsg:
                  type: integer
                  default: 4326
                  description: EPSG code the bbox is expressed in. 4326 and the project CRS (2039 by default)
                    are the tested values.
                groundPercentile:
                  type: number
                  default: 10.0
                  description: Percentile of sampled elevations treated as ground when deriving nDSM heights.
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_ARGUMENT
        '403':
          description: FORBIDDEN
        '409':
          description: ELEVATION_UNAVAILABLE — the project has no usable DSM
      operationId: postProjectsByProjectIdElevationSample
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: elevation
        surface: platform
        permission: layers.read
        crs: bboxEpsg, default 4326
        source: server/db/elevation_service.sample_aoi
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/elevation/sample
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/elevation/gate:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - elevation
      summary: Keep only the features whose nDSM height clears minHeightM. Used to drop ground-level false
        positives from a segmentation result.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - features
              - minHeightM
              properties:
                features:
                  type: array
                  items:
                    type: object
                  description: GeoJSON Features expressed in featureEpsg.
                minHeightM:
                  type: number
                  description: Metres above the derived ground surface.
                featureEpsg:
                  type: integer
                  default: 4326
                groundPercentile:
                  type: number
                  default: 10.0
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_ARGUMENT
        '403':
          description: FORBIDDEN
        '409':
          description: ELEVATION_UNAVAILABLE
      operationId: postProjectsByProjectIdElevationGate
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: elevation
        surface: platform
        permission: layers.read
        crs: featureEpsg, default 4326
        source: server/db/elevation_service.gate_features
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/elevation/gate
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/aois/register:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - viewer
      summary: Register an AOI against an immutable source revision (editor+). Idempotent per idempotencyKey.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - name
              - geometry
              properties:
                name:
                  type: string
                geometry:
                  type: object
                  description: GeoJSON geometry in EPSG:4326.
                realityModelId:
                  type: string
                  nullable: true
                sourceModelId:
                  type: string
                  nullable: true
                idempotencyKey:
                  type: string
                  nullable: true
      responses:
        '201':
          description: Created
        '400':
          description: INVALID_ARGUMENT
        '403':
          description: FORBIDDEN
      operationId: postProjectsByProjectIdAoisRegister
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: beta
        domain: viewer
        surface: platform
        role: editor
        idempotency: body
        crs: GeoJSON EPSG:4326
        source: server/db/aoi_registration_service.register_aoi
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/aois/register
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/cad-imports:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - imports
      summary: Convert an uploaded DWG/DXF/DGN drawing into GIS features (editor+). The 32MB body limit
        applies to this route.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - filename
              - contentBase64
              properties:
                filename:
                  type: string
                contentBase64:
                  type: string
                sourceEpsg:
                  type: integer
                  nullable: true
                  description: CRS the drawing coordinates are in. Omit only when the file carries a georeference.
                layerFilter:
                  type: array
                  items:
                    type: string
                  nullable: true
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_ARGUMENT
        '403':
          description: FORBIDDEN
        '413':
          description: PAYLOAD_TOO_LARGE
      operationId: postProjectsByProjectIdCadImports
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: imports
        surface: platform
        role: editor
        bodyLimitMb: 32
        source: server/db/cad_import_service.convert_cad_upload
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/cad-imports
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/architectural-model-imports:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: status
      in: query
      required: false
      schema:
        type: string
    get:
      tags:
      - imports
      summary: List architectural (BIM) import sessions.
      responses:
        '200':
          description: OK
        '403':
          description: FORBIDDEN
      operationId: getProjectsByProjectIdArchitecturalModelImports
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: imports
        surface: platform
        role: viewer
        source: server/db/architectural_model_service.list_imports
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/architectural-model-imports
        sourceDocument: server/db/openapi.yaml
    post:
      tags:
      - imports
      summary: Open an architectural-model import session (editor+). The session is the unit of revision;
        the bytes arrive at /inspect.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - name
              properties:
                name:
                  type: string
                sourceFormat:
                  type: string
                  nullable: true
                  description: 'IFC / IFCZIP / RVT / GLB / ... '
                originalFilename:
                  type: string
                  nullable: true
                classification:
                  type: string
                  nullable: true
                discipline:
                  type: string
                  nullable: true
                buildingEntityId:
                  type: string
                  nullable: true
                revision:
                  type: string
                  nullable: true
                issueDate:
                  type: string
                  nullable: true
                designStatus:
                  type: string
                  nullable: true
                revisionNotes:
                  type: string
                  nullable: true
                units:
                  type: string
                  nullable: true
                declaredCrsEpsg:
                  type: integer
                  nullable: true
                idempotencyKey:
                  type: string
                  nullable: true
                metadata:
                  type: object
                  nullable: true
      responses:
        '201':
          description: Created
        '400':
          description: INVALID_ARGUMENT
        '403':
          description: FORBIDDEN
      operationId: postProjectsByProjectIdArchitecturalModelImports
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: imports
        surface: platform
        role: editor
        idempotency: body
        source: server/db/architectural_model_service.create_import
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/architectural-model-imports
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/architectural-model-imports/{importId}:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: importId
      in: path
      required: true
      schema:
        type: string
    get:
      tags:
      - imports
      summary: 'Import session detail: parsed semantics, element counts, placement and publication state.'
      responses:
        '200':
          description: OK
        '404':
          description: NOT_FOUND
      operationId: getProjectsByProjectIdArchitecturalModelImportsByImportId
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: imports
        surface: platform
        role: viewer
        source: server/db/architectural_model_service.get_import
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/architectural-model-imports/eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/architectural-model-imports/{importId}/inspect:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: importId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - imports
      summary: Upload and parse the model bytes. IFC is read for SEMANTICS by a pure-python STEP parser;
        render geometry is a separate glTF/3D Tiles asset.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - filename
              - contentBase64
              properties:
                filename:
                  type: string
                contentBase64:
                  type: string
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_ARGUMENT
        '413':
          description: PAYLOAD_TOO_LARGE
        '422':
          description: VALIDATION_FAILED
      operationId: postProjectsByProjectIdArchitecturalModelImportsByImportIdInspect
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: imports
        surface: platform
        role: editor
        bodyLimitMb: 1
        source: server/db/architectural_model_service.inspect_import
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/architectural-model-imports/eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee/inspect
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/architectural-model-imports/{importId}/placement:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: importId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - imports
      summary: Georeference the model. Body {mode, originLon, originLat, originHeight, crsEpsg, trueNorthDeg,
        notes}.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - mode
              properties:
                mode:
                  type: string
                originLon:
                  type: number
                  nullable: true
                originLat:
                  type: number
                  nullable: true
                originHeight:
                  type: number
                  nullable: true
                crsEpsg:
                  type: integer
                  nullable: true
                trueNorthDeg:
                  type: number
                  nullable: true
                notes:
                  type: string
                  nullable: true
      responses:
        '200':
          description: OK
        '400':
          description: INVALID_ARGUMENT
      operationId: postProjectsByProjectIdArchitecturalModelImportsByImportIdPlacement
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: imports
        surface: platform
        role: editor
        crs: crsEpsg names the model's own CRS
        source: server/db/architectural_model_service.set_placement
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/architectural-model-imports/eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee/placement
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/architectural-model-imports/{importId}/publish:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: importId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - imports
      summary: 'Publish the import as a Reality Model revision. Same rule as POST /models: no storagePrefix,
        no renderRootKey — the location is derived, not declared.'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - datasetKey
              - tilesetUrl
              properties:
                datasetKey:
                  type: string
                tilesetUrl:
                  type: string
                storageBucket:
                  type: string
                  nullable: true
                renderRootFilename:
                  type: string
                  nullable: true
                bounds:
                  type: object
                  nullable: true
                version:
                  type: integer
                  nullable: true
                activate:
                  type: boolean
                  default: false
      responses:
        '200':
          description: OK
        '400':
          description: UNKNOWN_FIELD / INVALID_ARGUMENT
        '403':
          description: FORBIDDEN
      operationId: postProjectsByProjectIdArchitecturalModelImportsByImportIdPublish
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: imports
        surface: platform
        role: editor
        source: server/db/architectural_model_service.publish_import
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/architectural-model-imports/eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee/publish
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/architectural-model-imports/{importId}/cancel:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: importId
      in: path
      required: true
      schema:
        type: string
    post:
      tags:
      - imports
      summary: Abandon an unpublished import session. Body {reason}.
      responses:
        '200':
          description: OK
        '409':
          description: INVALID_STATE
      operationId: postProjectsByProjectIdArchitecturalModelImportsByImportIdCancel
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: imports
        surface: platform
        role: editor
        source: server/db/architectural_model_service.cancel_import
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/architectural-model-imports/eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee/cancel
        sourceDocument: server/db/openapi.yaml
  /api/projects/{projectId}/bim-models/{modelId}/features/{featureKey}:
    parameters:
    - name: projectId
      in: path
      required: true
      schema:
        type: string
    - name: modelId
      in: path
      required: true
      schema:
        type: string
    - name: featureKey
      in: path
      required: true
      schema:
        type: string
      description: The BIM element key (GlobalId or the import's stable element key).
    get:
      tags:
      - imports
      summary: Property set of one BIM element — what the Viewer shows when an element is picked.
      responses:
        '200':
          description: OK
        '404':
          description: NOT_FOUND
      operationId: getProjectsByProjectIdBimModelsByModelIdFeaturesByFeatureKey
      security:
      - firebaseIdToken: []
      - devUserHeader: []
      x-skylens:
        status: stable
        domain: imports
        surface: platform
        role: viewer
        source: server/db/architectural_model_service.get_bim_feature
        examplePath: /api/projects/11111111-1111-4111-8111-111111111111/bim-models/44444444-4444-4444-8444-444444444444/features/1a2B3c4D5e6F7g8H9i0Jk
        sourceDocument: server/db/openapi.yaml
  /api/partners/v1/external-projects:
    get:
      summary: List external projects (cursor-paginated)
      description: 'Scoped to the verified partner. Ordered by `(updated_at DESC,

        partnerProjectId DESC)` — a deterministic tie-breaker, so a page boundary

        is stable even when many rows share a timestamp. One SQL query per page

        regardless of page size; backed by `idx_partner_projects_catalog`.

        '
      parameters:
      - name: cursor
        in: query
        schema:
          type: string
      - name: limit
        in: query
        schema:
          type: integer
          default: 25
          maximum: 100
      - name: externalTenantId
        in: query
        schema:
          type: string
      - name: status
        in: query
        schema:
          type: string
          enum:
          - active
          - archived
      - name: search
        in: query
        schema:
          type: string
          maxLength: 200
        description: Substring match on external id or display name.
      - $ref: '#/components/parameters/RequestId'
      responses:
        '200':
          description: A page of external projects.
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      type: object
                      properties:
                        externalProjects:
                          type: array
                          items:
                            $ref: '#/components/schemas/ExternalProject'
                        nextCursor:
                          type:
                          - string
                          - 'null'
        '401':
          description: INVALID_CREDENTIAL
        '403':
          description: SCOPE_DENIED | PARTNER_SUSPENDED
        '429':
          description: RATE_LIMITED
      operationId: getPartnersV1ExternalProjects
      security:
      - partnerCredential:
        - external_projects:read
      tags:
      - partners
      x-skylens:
        status: stable
        domain: partners
        surface: partner
        scope: external_projects:read
        paginated: cursor
        source: server/db/partner_api.py (EXTERNAL_PROJECTS_COLLECTION)
        examplePath: /api/partners/v1/external-projects
        sourceDocument: server/db/partner_openapi.yaml
  /api/partners/v1/external-projects/{externalProjectId}:
    parameters:
    - name: externalProjectId
      in: path
      required: true
      schema:
        type: string
        minLength: 1
        maxLength: 200
    get:
      summary: Retrieve one external project
      description: '`externalTenantId` is required when the same external id exists under

        more than one of the partner''s tenants; otherwise the request is

        `INVALID_ARGUMENT` (ambiguous), never an arbitrary pick.

        '
      parameters:
      - name: externalTenantId
        in: query
        schema:
          type: string
      - $ref: '#/components/parameters/RequestId'
      responses:
        '200':
          description: The external project and its current snapshot.
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      $ref: '#/components/schemas/ExternalProject'
        '400':
          description: INVALID_ARGUMENT (ambiguous across tenants)
        '404':
          description: 'NOT_FOUND. Also returned for another partner''s project — the response

            is identical whether or not the resource exists.

            '
      operationId: getPartnersV1ExternalProjectsByExternalProjectId
      security:
      - partnerCredential:
        - external_projects:read
      tags:
      - partners
      x-skylens:
        status: stable
        domain: partners
        surface: partner
        scope: external_projects:read
        source: server/db/partner_api.py (EXTERNAL_PROJECT_ITEM)
        examplePath: /api/partners/v1/external-projects/ext_demo_001
        sourceDocument: server/db/partner_openapi.yaml
    put:
      summary: Create or update an external project (append-only snapshot)
      description: 'Upserts the stable catalog record and appends an immutable snapshot.


        Idempotent at two levels: an identical body appends no snapshot and

        returns the existing one (content-addressed by sha256), independently of

        the `Idempotency-Key` replay cache.


        **Creates no `core.projects` or `core.organizations` row.** Naming

        another partner''s tenant does not reach it — it creates a same-named

        tenant under the caller''s own partner.

        '
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      - $ref: '#/components/parameters/RequestId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExternalProjectUpsert'
      responses:
        '200':
          description: Existing record updated.
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      $ref: '#/components/schemas/ExternalProject'
        '201':
          description: Stable record created.
        '400':
          description: 'EXTERNAL_PROJECT_INVALID | UNKNOWN_FIELD | UNSUPPORTED_VERSION |

            IDEMPOTENCY_KEY_REQUIRED | INVALID_ARGUMENT

            '
        '403':
          description: SCOPE_DENIED (credential lacks external_projects:write)
        '409':
          description: 'IDEMPOTENCY_CONFLICT (same key, different body) |

            IDEMPOTENCY_IN_PROGRESS (concurrent request holds the key) |

            SNAPSHOT_VERSION_CONFLICT (same sourceVersion, different body) |

            EXTERNAL_TENANT_INACTIVE

            '
        '413':
          description: PAYLOAD_TOO_LARGE (> 4 MiB)
        '429':
          description: RATE_LIMITED
      operationId: putPartnersV1ExternalProjectsByExternalProjectId
      security:
      - partnerCredential:
        - external_projects:write
      tags:
      - partners
      x-skylens:
        status: stable
        domain: partners
        surface: partner
        scope: external_projects:write
        idempotency: header
        source: server/db/partner_api.py (EXTERNAL_PROJECT_ITEM)
        examplePath: /api/partners/v1/external-projects/ext_demo_001
        sourceDocument: server/db/partner_openapi.yaml
  /api/partners/v1/services:
    get:
      summary: List services the partner is entitled to sell
      description: 'Scoped to the partner''s ACTIVE CONTRACT and explicit entitlements. An

        unentitled service is absent, not merely hidden — and its detail route

        returns 404, so entitlement cannot be probed.


        Feeds the customer widget''s service cards. `fromPriceMinor` is a display

        anchor only; the authoritative amount always comes from a quote.

        '
      parameters:
      - name: locale
        in: query
        schema:
          type: string
        description: Selects the copy variant. Production defaults to the repository's English rule; unknown
          locales fall back to it rather than to null. Hebrew/RTL is enabled by configuration, not code.
      - $ref: '#/components/parameters/RequestId'
      responses:
        '200':
          description: Entitled services.
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      type: object
                      properties:
                        services:
                          type: array
                          items:
                            $ref: '#/components/schemas/Service'
        '403':
          description: SCOPE_DENIED | PARTNER_SUSPENDED | SERVICE_NOT_AVAILABLE (no active contract)
      operationId: getPartnersV1Services
      security:
      - partnerCredential:
        - services:read
      tags:
      - partners
      x-skylens:
        status: stable
        domain: partners
        surface: partner
        scope: services:read
        source: server/db/partner_api.py (SERVICES_COLLECTION)
        examplePath: /api/partners/v1/services
        sourceDocument: server/db/partner_openapi.yaml
  /api/partners/v1/services/{serviceCode}:
    parameters:
    - name: serviceCode
      in: path
      required: true
      schema:
        type: string
        pattern: ^[a-z0-9_]{1,64}$
    get:
      summary: Retrieve one service with its calculator specification
      description: 'Adds the declarative `calculatorInputs` the widget renders from, the

        required project facts, assumptions and exclusions. The frontend never

        hardcodes a field list or its copy.

        '
      parameters:
      - name: locale
        in: query
        schema:
          type: string
      - $ref: '#/components/parameters/RequestId'
      responses:
        '200':
          description: Service detail.
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      $ref: '#/components/schemas/ServiceDetail'
        '404':
          description: NOT_FOUND — absent, or not entitled for this partner
      operationId: getPartnersV1ServicesByServiceCode
      security:
      - partnerCredential:
        - services:read
      tags:
      - partners
      x-skylens:
        status: stable
        domain: partners
        surface: partner
        scope: services:read
        source: server/db/partner_api.py (SERVICE_ITEM)
        examplePath: /api/partners/v1/services/orthophoto_survey
        sourceDocument: server/db/partner_openapi.yaml
  /api/partners/v1/quotes:
    post:
      summary: Calculate and persist an immutable quote
      description: 'Prices the request server-side from the versioned service, pricing and

        rule configuration, against the project''s immutable snapshot.


        The quote records every version used (service, pricing, contract,

        snapshot) plus an `inputHash`, so it is reproducible and is never

        re-priced by a later pricing version.


        Outcome is one of `fixed`, `range` or `manual_review` — see the Quote

        schema. Acceptance is Phase 3 and is not available here.

        '
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      - $ref: '#/components/parameters/RequestId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QuoteRequest'
      responses:
        '201':
          description: Quote calculated.
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      $ref: '#/components/schemas/Quote'
        '400':
          description: 'UNKNOWN_FIELD (includes any client-submitted total) |

            UNSUPPORTED_VERSION | INVALID_ARGUMENT | IDEMPOTENCY_KEY_REQUIRED

            '
        '403':
          description: SCOPE_DENIED | SERVICE_NOT_AVAILABLE
        '404':
          description: NOT_FOUND — service, project or snapshot
        '409':
          description: IDEMPOTENCY_CONFLICT | IDEMPOTENCY_IN_PROGRESS
      operationId: postPartnersV1Quotes
      security:
      - partnerCredential:
        - quotes:write
      tags:
      - partners
      x-skylens:
        status: stable
        domain: partners
        surface: partner
        scope: quotes:write
        idempotency: header
        source: server/db/partner_api.py (QUOTES_COLLECTION)
        examplePath: /api/partners/v1/quotes
        sourceDocument: server/db/partner_openapi.yaml
  /api/partners/v1/quotes/{quoteId}:
    parameters:
    - name: quoteId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    get:
      summary: Retrieve a quote
      description: 'Scoped to the verified partner; another partner''s quote is 404. `status`

        is derived at read time — a quote past `validUntil` reports `expired` and

        `orderable: false` without any write.

        '
      parameters:
      - name: locale
        in: query
        schema:
          type: string
      - $ref: '#/components/parameters/RequestId'
      responses:
        '200':
          description: The quote.
          content:
            application/json:
              schema:
                allOf:
                - $ref: '#/components/schemas/Envelope'
                - type: object
                  properties:
                    data:
                      $ref: '#/components/schemas/Quote'
        '404':
          description: NOT_FOUND
      operationId: getPartnersV1QuotesByQuoteId
      security:
      - partnerCredential:
        - quotes:read
      tags:
      - partners
      x-skylens:
        status: stable
        domain: partners
        surface: partner
        scope: quotes:read
        source: server/db/partner_api.py (QUOTE_ITEM)
        examplePath: /api/partners/v1/quotes/ffffffff-ffff-4fff-8fff-ffffffffffff
        sourceDocument: server/db/partner_openapi.yaml
components:
  schemas:
    DraftApprove:
      allOf:
      - $ref: '#/components/schemas/DraftDecision'
      - type: object
        properties:
          observedAt:
            type: string
            format: date-time
            nullable: true
            description: When the underlying observation was made, if it differs from the approval time.
    DraftDecision:
      type: object
      required:
      - expectedRevision
      - reason
      - idempotencyKey
      properties:
        expectedRevision:
          type: integer
          description: The revision the caller read. A stale value is 409 VERSION_CONFLICT — the edit
            is never silently applied on top of someone else's.
        reason:
          type: string
          description: Recorded in the audit trail; required on every review decision.
        idempotencyKey:
          type: string
    DraftGeometryEdit:
      allOf:
      - $ref: '#/components/schemas/DraftDecision'
      - type: object
        required:
        - geometry
        properties:
          geometry:
            type: object
            description: GeoJSON geometry, EPSG:4326.
    DraftReclassify:
      allOf:
      - $ref: '#/components/schemas/DraftDecision'
      - type: object
        required:
        - targetLayerId
        - targetEntityTypeId
        properties:
          targetLayerId:
            type: string
          targetEntityTypeId:
            type: string
            description: Must exist in shared/entity_registry.json.
    DraftRegenerate:
      allOf:
      - $ref: '#/components/schemas/DraftDecision'
      - type: object
        properties:
          options:
            type: object
            nullable: true
    DraftSplit:
      allOf:
      - $ref: '#/components/schemas/DraftDecision'
      - type: object
        required:
        - geometries
        properties:
          geometries:
            type: array
            items:
              type: object
            description: Two or more GeoJSON geometries, EPSG:4326.
    Envelope:
      type: object
      required:
      - data
      - meta
      - error
      properties:
        data:
          description: Payload on success; null on error
        meta:
          type: object
          required:
          - requestId
          - timestamp
          - contractVersion
          properties:
            requestId:
              type: string
            timestamp:
              type: string
              format: date-time
            contractVersion:
              type: string
              const: '1.0'
        error:
          oneOf:
          - type: 'null'
          - type: object
            required:
            - code
            - message
            - retryable
            properties:
              code:
                type: string
              message:
                type: string
              retryable:
                type: boolean
    Error:
      type: object
      description: 'The platform API''s error envelope, as implemented. NOTE: this is NOT the {data, meta,
        error} envelope in docs/contracts/API.md §2.2 — only the Partner API implements that. Successes
        are bare objects with correlationId and traceId merged in at the top level.'
      required:
      - error
      properties:
        error:
          type: object
          required:
          - code
          - message
          properties:
            code:
              type: string
              description: Stable machine code, e.g. NOT_FOUND, FORBIDDEN, INVALID_ARGUMENT, VERSION_CONFLICT.
            message:
              type: string
              description: Caller-safe text. An uncoded internal fault is always the generic 'internal
                error'; the real detail is logged server-side under the same correlationId.
            retryable:
              type: boolean
              description: True when repeating the identical request may succeed (e.g. QUERY_TIMEOUT).
            correlationId:
              type: string
              description: Quote this when contacting support.
    ExternalProject:
      type: object
      properties:
        partnerProjectId:
          type: string
          format: uuid
        externalProjectId:
          type: string
        externalTenantId:
          type: string
        displayName:
          type: string
        status:
          type: string
          enum:
          - active
          - archived
        snapshotCount:
          type: integer
        sourceUpdatedAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        skylensProjectLinked:
          type: boolean
          description: 'Always false in Phase 1. Present so the contract does not change

            shape when lazy activation populates it.

            '
        currentSnapshot:
          type: object
          description: 'Normalized, queryable facts plus provenance. These are the inputs the

            Phase 2 pricing calculator will read.

            '
          properties:
            snapshotId:
              type: string
              format: uuid
            snapshotHash:
              type: string
              description: sha256 of the canonical payload
            sourceVersion:
              type: string
            schemaVersion:
              type: string
            name:
              type: string
            projectType:
              type: string
            constructionStage:
              type: string
            address:
              type: string
            areaSqm:
              type: number
            parcelCount:
              type: integer
            buildingCount:
              type: integer
            maxBuildingHeightM:
              type: number
            receivedAt:
              type: string
              format: date-time
            location:
              type: object
              description: GeoJSON Point (EPSG:4326)
            boundary:
              type: object
              description: GeoJSON MultiPolygon (EPSG:4326)
    ExternalProjectUpsert:
      type: object
      required:
      - schemaVersion
      - externalTenantId
      additionalProperties: false
      properties:
        schemaVersion:
          type: string
          enum:
          - '1.0'
        externalTenantId:
          type: string
          minLength: 1
          maxLength: 200
        externalUserId:
          type: string
          maxLength: 200
        sourceVersion:
          type: string
          maxLength: 100
          description: 'Partner-side version marker. Reusing one with a different body is a

            `SNAPSHOT_VERSION_CONFLICT`: provenance must not fork silently.

            '
        name:
          type: string
          maxLength: 300
        status:
          type: string
          enum:
          - active
          - archived
        projectType:
          type: string
          maxLength: 100
        constructionStage:
          type: string
          maxLength: 100
        address:
          type: string
          maxLength: 500
        location:
          type: object
          properties:
            longitude:
              type: number
            latitude:
              type: number
            crs:
              type: string
              default: EPSG:4326
        boundary:
          type: object
          description: 'GeoJSON Polygon or MultiPolygon, max 50,000 vertices. Stored as

            canonical EPSG:2039 with EPSG:4326 derived by trigger. Structure is

            validated in Python before PostGIS sees it, so a malformed geometry

            can never abort the surrounding transaction.

            '
          properties:
            geometry:
              type: object
            crs:
              type: string
              default: EPSG:4326
        areaSqm:
          type: number
          minimum: 0
          description: Derived from the boundary when omitted.
        parcels:
          type: array
          maxItems: 2000
          items:
            type: object
        buildings:
          type: array
          maxItems: 2000
          items:
            type: object
        contacts:
          type: array
          maxItems: 50
          items:
            type: object
        documents:
          type: array
          maxItems: 200
          items:
            type: object
        dates:
          type: object
        customerOrganization:
          type: object
        permissions:
          type: object
        deliveryPreferences:
          type: object
        metadata:
          type: object
          maxProperties: 100
        updatedAt:
          type: string
          format: date-time
    IdempotentCommand:
      type: object
      required:
      - idempotencyKey
      properties:
        idempotencyKey:
          type: string
          description: Required in the BODY for the extraction lifecycle commands (these predate the Idempotency-Key
            header).
    ModelList:
      type: object
      properties:
        models:
          type: array
          items:
            type: object
            description: Client projection — storagePrefix, bucket and rootKey are deliberately absent.
            properties:
              modelId:
                type: string
                format: uuid
              datasetKey:
                type: string
              name:
                type: string
              modelType:
                type: string
              version:
                type: integer
              status:
                type: string
              bounds:
                type: object
                nullable: true
              captureDate:
                type: string
                nullable: true
    Page:
      type: object
      description: Cursor pagination. nextCursor is opaque and null on the final page. Collections that
        are NOT paginated return a plain array under a named key instead.
      properties:
        nextCursor:
          type: string
          nullable: true
    Project:
      type: object
      properties:
        projectId:
          type: string
          format: uuid
        organizationId:
          type: string
          format: uuid
        name:
          type: string
        slug:
          type: string
          nullable: true
        projectType:
          type: string
          nullable: true
        status:
          type: string
          enum:
          - draft
          - active
          - on_hold
          - completed
          - archived
        accessMode:
          type: string
        defaultCrsEpsg:
          type: integer
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    ProjectPage:
      allOf:
      - $ref: '#/components/schemas/Page'
      - type: object
        properties:
          projects:
            type: array
            items:
              $ref: '#/components/schemas/Project'
    Quote:
      type: object
      description: 'Customer-safe by construction. There is no commission, cost, margin or

        contract-version field: the projection names every field it emits.

        '
      properties:
        quoteId:
          type: string
          format: uuid
        quoteNumber:
          type: string
        quoteType:
          type: string
          enum:
          - fixed
          - range
          - manual_review
        status:
          type: string
          enum:
          - calculated
          - expired
          - superseded
          - accepted
          description: '`expired` is derived at read time from validUntil.'
        orderable:
          type: boolean
          description: True only for a live `fixed` quote. Phase 3 will allow accepting exactly these;
            a range or manual-review quote is never orderable.
        currency:
          type: string
        serviceCode:
          type: string
        serviceName:
          type: string
        serviceVersion:
          type: integer
        pricingVersion:
          type: integer
        externalProjectId:
          type: string
        externalTenantId:
          type: string
        amounts:
          type: object
          description: '`fixed`: subtotalMinor, discountMinor, taxMinor, totalMinor.

            `range`: rangeMinMinor, rangeMaxMinor (never a binding total).

            `manual_review`: empty.

            '
          properties:
            subtotalMinor:
              type: integer
            discountMinor:
              type: integer
            taxMinor:
              type: integer
            totalMinor:
              type: integer
            rangeMinMinor:
              type: integer
            rangeMaxMinor:
              type: integer
        tax:
          type: object
          properties:
            mode:
              type: string
              enum:
              - exclusive
              - inclusive
              - none
            rateBps:
              type: integer
            code:
              type:
              - string
              - 'null'
            amountMinor:
              type:
              - integer
              - 'null'
        breakdown:
          type: array
          description: Customer-safe line items only.
          items:
            type: object
            properties:
              code:
                type: string
              type:
                type: string
              amountMinor:
                type: integer
              labelKey:
                type: string
        assumptions:
          type: array
          items:
            type: object
        exclusions:
          type: array
          items:
            type: object
        missingFacts:
          type: array
          items:
            type: string
          description: Facts whose absence prevented a fixed price.
        reviewReasons:
          type: array
          items:
            type: object
          description: Why the quote needs Skylens review.
        calculatorInputs:
          type: object
        projectFacts:
          type: object
          description: The snapshot facts actually priced — pinned, not current.
        inputHash:
          type: string
          description: sha256 of versions + facts + inputs; determinism anchor.
        validUntil:
          type:
          - string
          - 'null'
          format: date-time
        createdAt:
          type: string
          format: date-time
    QuoteRequest:
      type: object
      required:
      - schemaVersion
      - externalTenantId
      - externalProjectId
      - serviceCode
      additionalProperties: false
      properties:
        schemaVersion:
          type: string
          enum:
          - '1.0'
        externalTenantId:
          type: string
          maxLength: 200
        externalProjectId:
          type: string
          maxLength: 200
        externalUserId:
          type: string
          maxLength: 200
        serviceCode:
          type: string
          maxLength: 200
        snapshotId:
          type: string
          format: uuid
          description: Price against a specific snapshot. Defaults to the project's current snapshot.
            A snapshot belonging to another project is NOT FOUND.
        locale:
          type: string
        inputs:
          type: object
          maxProperties: 50
          description: Scalar calculator inputs only. Objects and arrays are rejected, as is any monetary
            field.
    Service:
      type: object
      description: Customer-safe service card. Contains no commission or cost.
      properties:
        serviceCode:
          type: string
        category:
          type: string
          enum:
          - progress_monitoring
          - reality_model
          - orthophoto_measurement
          - marketing_media
        name:
          type: string
          description: Resolved for the requested locale
        description:
          type: string
        iconToken:
          type: string
        serviceVersion:
          type: integer
        pricingVersion:
          type: integer
        currency:
          type: string
          description: ISO 4217
        fromPriceMinor:
          type: integer
          description: Integer MINOR units. Display anchor only, not a quote.
        requiredProjectFacts:
          type: array
          items:
            type: string
        supportedDeliverables:
          type: array
          items:
            type: string
        quoteValidityHours:
          type: integer
        taxTreatment:
          type: object
          description: Configuration, never a constant.
          properties:
            mode:
              type: string
              enum:
              - exclusive
              - inclusive
              - none
            rateBps:
              type: integer
              description: Basis points; 1700 = 17%
            code:
              type:
              - string
              - 'null'
    ServiceDetail:
      allOf:
      - $ref: '#/components/schemas/Service'
      - type: object
        properties:
          minimumTotalMinor:
            type: integer
          sla:
            type: object
          assumptions:
            type: array
            items:
              type: object
              properties:
                code:
                  type:
                  - string
                  - 'null'
                text:
                  type: string
          exclusions:
            type: array
            items:
              type: object
              properties:
                code:
                  type:
                  - string
                  - 'null'
                text:
                  type: string
          calculatorInputs:
            type: array
            description: Declarative input spec the widget renders. Labels are already locale-resolved,
              so no copy is hardcoded in the frontend.
            items:
              type: object
              properties:
                key:
                  type: string
                type:
                  type: string
                  enum:
                  - integer
                  - number
                  - enum
                  - boolean
                required:
                  type: boolean
                label:
                  type: string
                unit:
                  type:
                  - string
                  - 'null'
                min:
                  type: number
                max:
                  type: number
                default: {}
                options:
                  type: array
                  items:
                    type: object
                    properties:
                      value: {}
                      label:
                        type: string
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 8
        maxLength: 200
      description: 'Required on every write. Replaying a key with an identical body returns

        the stored response with `replayed: true`; replaying it with a different

        body returns `IDEMPOTENCY_CONFLICT`. Keys are scoped per API consumer, so

        two partners cannot collide.

        '
    IdempotencyKeyRequired:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 1
        maxLength: 200
      description: 'REQUIRED replay-safe key. Replaying it returns the prior result instead of acting
        twice; reusing one key for a different target is 409 DUPLICATE_REQUEST. Note the scope: the header
        is honoured by the membership commands, the reality-attach approval and the agent-memory routes.
        Elsewhere the key travels in the body as `idempotencyKey`, and several routes (exports, document
        upload) honour neither — see x-skylens-classification.'
    RequestId:
      name: X-Request-ID
      in: header
      required: false
      schema:
        type: string
        maxLength: 200
      description: Echoed back in the response header and in `meta.requestId`.
  securitySchemes:
    devUserHeader:
      type: apiKey
      in: header
      name: X-Skylens-User-Id
      description: 'Development only: the internal principal UUID, trusted verbatim. Never use it against
        a deployed environment.'
    firebaseIdToken:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: 'Authorization: Bearer <Firebase ID token>. The only authentication a browser or external
        client should use.'
    partnerCredential:
      type: http
      scheme: bearer
      description: '`Authorization: Bearer <keyId>.<secret>`


        The secret is a 256-bit random value stored only as a SHA-256 hash and

        shown exactly once at issuance. Credentials carry scopes, a status

        (`active` | `rotating` | `revoked`) and an optional expiry; all are

        re-checked on every request, so revocation is immediate.


        Scopes: `external_projects:read`, `external_projects:write`,

        `services:read`, `quotes:read`, `quotes:write`. They are separately

        grantable — a partner may sync projects without being able to price them.


        Never place a partner credential in browser JavaScript.

        '
x-skylens:
  contractVersion: '1'
  domains:
  - id: getting-started
    title: Getting Started
    kind: guide
  - id: authentication
    title: Authentication
    kind: guide
  - id: projects
    title: Projects
    kind: reference
  - id: documents
    title: Documents
    kind: reference
  - id: gis
    title: GIS Layers and Features
    kind: reference
  - id: spatial-analysis
    title: Spatial Analysis
    kind: reference
  - id: elevation
    title: Elevation
    kind: reference
  - id: entities
    title: Entities
    kind: reference
  - id: review
    title: Entity Review
    kind: reference
  - id: reality-models
    title: Reality Models
    kind: reference
  - id: imports
    title: CAD and BIM Imports
    kind: reference
  - id: photos
    title: Photos and 360 Panoramas
    kind: reference
  - id: ai-jobs
    title: AI Jobs
    kind: reference
  - id: agent-memory
    title: Agent Memory
    kind: reference
  - id: scene-graph
    title: Scene Graph
    kind: reference
  - id: viewer
    title: Viewer
    kind: reference
  - id: chat-database
    title: Chat Database Grounding
    kind: reference
  - id: export
    title: Export
    kind: reference
  - id: partners
    title: Partner Services
    kind: reference
  - id: errors
    title: Errors
    kind: guide
  - id: pagination
    title: Pagination and Idempotency
    kind: guide
  - id: geometry
    title: Geometry and CRS
    kind: guide
  - id: changelog
    title: Changelog
    kind: guide
  surfaces:
    platform:
      title: Skylens Platform API
      router: server/db/extraction_api.py
      localBaseUrl: http://127.0.0.1:8600
      auth:
      - firebaseIdToken
      - devUserHeader
      envelope: bare
    entity:
      title: Entity API
      router: server/db/entity_api.py
      localBaseUrl: http://127.0.0.1:8500
      auth:
      - devUserHeader
      envelope: bare
    partner:
      title: Partner Services API
      router: server/db/partner_api.py
      localBaseUrl: http://127.0.0.1:8700
      auth:
      - partnerCredential
      envelope: data-meta-error
    firebase:
      title: Firebase Hosting rewrites
      router: functions/index.js
      localBaseUrl: https://skylens-viewer.web.app
      auth: []
      envelope: bare
  environments:
  - id: local
    title: Local development
    baseUrl: http://127.0.0.1:8600
    configured: true
    tryIt: true
    note: 'Started by `npm run dev`. Identity is the X-Skylens-User-Id header, trusted verbatim because
      no gateway fronts it. CORS is Access-Control-Allow-Origin: * on this surface.'
  - id: production
    title: Production
    baseUrl: https://api.example.skylens.co.il
    configured: false
    tryIt: false
    note: 'PLACEHOLDER — requires deployment configuration. The gateway (functions/project-api-gateway.js)
      is implemented and unit-tested but not wired: firebase.json has no "/api/**" -> projectApiGateway
      rewrite, and SKYLENS_API_UPSTREAM_URL / SKYLENS_CLOUD_RUN_AUDIENCE / SKYLENS_GATEWAY_SHARED_SECRET
      are unset. Until then this host resolves to nothing and Try It is disabled against it.'
  planned:
  - method: GET
    path: /api/projects/{projectId}/summary
    domain: projects
    documentedIn: docs/contracts/API.md (from the retired 07_SKYLENS_API_CONTRACT)
    note: No route regex matches. GET /api/projects/{projectId} already returns a content inventory.
  - method: GET
    path: /api/projects/{projectId}/viewer/bootstrap
    domain: viewer
    documentedIn: three skill contracts
    note: Referenced as the canonical Viewer bootstrap; implemented on no surface.
  - method: GET
    path: /api/projects/{projectId}/viewer/layers
    domain: viewer
    documentedIn: docs/contracts/API.md
    note: The implemented reads are /viewer/source-layers and /my-layers.
  - method: PATCH
    path: /api/projects/{projectId}/viewer/preferences
    domain: viewer
    documentedIn: docs/contracts/API.md
    note: No preferences route exists.
  - method: POST
    path: /api/projects/{projectId}/viewer/sessions
    domain: viewer
    documentedIn: docs/contracts/API.md
    note: No session route exists.
  - method: POST
    path: /api/projects/{projectId}/spatial-query
    domain: gis
    documentedIn: docs/contracts/API.md
    note: The implemented spatial reads are GET /gis-layers/{code}/features (bbox) and POST /spatial-analysis/overlays.
  - method: GET
    path: /api/projects/{projectId}/layers/{layerId}/features
    domain: gis
    documentedIn: docs/contracts/API.md
    note: This exact path exists on no surface; the code has /layer-features and /gis-layers/{code}/features.
  - method: GET
    path: /api/projects/{projectId}/entities
    domain: entities
    documentedIn: docs/contracts/API.md
    note: Entity CRUD is FLAT (/api/entities?projectId=) on the withheld :8500 surface, not project-scoped
      in the path.
  - method: GET
    path: /api/projects/{projectId}/entities/{entityId}/versions
    domain: entities
    documentedIn: docs/contracts/API.md
    note: entity_service.get_entity_versions exists in-process; no HTTP route reaches it.
  - method: GET
    path: /api/projects/{projectId}/entities/{entityId}/evidence
    domain: entities
    documentedIn: docs/contracts/API.md
    note: No route.
  - method: GET
    path: /api/projects/{projectId}/entities/{entityId}/captures
    domain: entities
    documentedIn: docs/contracts/API.md
    note: No route.
  - method: GET
    path: /api/projects/{projectId}/entities/{entityId}/documents
    domain: entities
    documentedIn: docs/contracts/API.md
    note: No route.
  - method: GET
    path: /api/projects/{projectId}/entities/{entityId}/tasks
    domain: entities
    documentedIn: docs/contracts/API.md
    note: No route, and no tasks.manage action in server/db/authz_actions.py.
  - method: GET
    path: /api/projects/{projectId}/reality-models
    domain: reality-models
    documentedIn: docs/contracts/API.md
    note: The implemented collection is GET /api/projects/{projectId}/models. Only the .../reality-models/{modelId}/delivery
      sub-path uses this spelling.
  - method: GET
    path: /api/projects/{projectId}/tilesets/{tilesetId}
    domain: reality-models
    documentedIn: docs/contracts/API.md
    note: No route. Tiles are delivered by CDN under a signed grant.
  - method: POST
    path: /api/projects/{projectId}/reality-models/{modelId}/activate
    domain: reality-models
    documentedIn: docs/contracts/API.md
    note: No route; activation happens at import publish time.
  - method: GET
    path: /api/projects/{projectId}/scene-graph/entities/{entityId}
    domain: scene-graph
    documentedIn: skylens-manage-admin-control-plane skill contract
    note: server/db/scene_graph_service.py is reached only in-process. NO HTTP surface exists for the
      scene graph.
  - method: POST
    path: /api/projects/{projectId}/scene-graph/query
    domain: scene-graph
    documentedIn: skill contract
    note: No HTTP surface.
  - method: GET
    path: /api/projects/{projectId}/scene-graph/nodes/{nodeId}/neighbors
    domain: scene-graph
    documentedIn: skill contract
    note: No HTTP surface.
  - method: POST
    path: /api/projects/{projectId}/scene-graph/edges
    domain: scene-graph
    documentedIn: skill contract
    note: No HTTP surface.
  - method: POST
    path: /api/projects/{projectId}/ai/jobs
    domain: ai-jobs
    documentedIn: docs/contracts/API.md
    note: 'The implemented lifecycle is POST /extraction-plans then POST /extraction-jobs. Deliberately
      two steps: a plan is confirmed before it costs anything.'
  - method: GET
    path: /api/projects/{projectId}/ai/jobs/{jobId}/results
    domain: ai-jobs
    documentedIn: docs/contracts/API.md
    note: Results are published as ai_draft layers and read through the review queue.
  demoIdentifiers:
    projectId: 11111111-1111-4111-8111-111111111111
    organizationId: 22222222-2222-4222-8222-222222222222
    userId: 33333333-3333-4333-8333-333333333333
    modelId: 44444444-4444-4444-8444-444444444444
    draftId: 55555555-5555-4555-8555-555555555555
    jobId: 66666666-6666-4666-8666-666666666666
    planId: 77777777-7777-4777-8777-777777777777
    collectionId: 88888888-8888-4888-8888-888888888888
    assetId: 99999999-9999-4999-8999-999999999999
    resourceId: aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa
    runId: bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb
    storeId: cccccccc-cccc-4ccc-8ccc-cccccccccccc
    dreamId: dddddddd-dddd-4ddd-8ddd-dddddddddddd
    importId: eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee
    quoteId: ffffffff-ffff-4fff-8fff-ffffffffffff
    layerId: layer_demo_001
    layerCode: planning.urban_renewal_declared
    entityType: building
    featureKey: 1a2B3c4D5e6F7g8H9i0Jk
    format: geojson
    serviceCode: orthophoto_survey
    externalProjectId: ext_demo_001
    aoiId: aoi_demo_001
    entityId: entity_demo_001
    observationId: observation_demo_001
    token: grant_demo_token
    workerId: worker_demo_001
    taskId: task_demo_001
    artifactId: artifact_demo_001
    candidateId: candidate_demo_001
    dreamId2: dream_demo_001
  census:
    declaredOperations: 160
    byStatus:
      stable: 66
      beta: 31
      internal: 58
      deprecated: 5
    byVisibility:
      public: 97
      internal: 63
    published: 97
    withheld: 63
    plannedNotOperations: 22
    note: 'byStatus and byVisibility are two views of the SAME declaredOperations set and must not be
      added together. plannedNotOperations is a DISJOINT set: those routes have no path on any surface
      and are not counted as operations.'
