openapi: 3.0.3
info:
  title: Repzo API - AI Object Detection Session
  version: 1.0.0
  description: |
    **Object Detection Sessions** are the parent record for a single
    AR shelf scan captured by the mobile app. A session groups N
    `ai-object-detection-task` rows (one per submitted frame) and adds
    aggregate 3D scene state, so the existing task-based training and
    review pipeline is reused unchanged — a session is a thin parent,
    the frame is still the atomic annotatable unit.

    **Who calls it.** Reps create sessions implicitly when the device
    posts its first frame to `/activity-ai-object-detection-session-frame`
    (which upserts the session). Admins and reps list and inspect
    sessions from the dashboard. Detection/placement is triggered later,
    per task, via `ai-object-detection-inference`, and cross-frame fusion
    runs through `ai-object-detection-session-analysis` (its own
    versioned collection — see below).

    **Multi-tenancy & lifecycle.** Records are scoped by
    `company_namespace[]` (server-injected from the caller's session).
    Soft-delete via `disabled: true`. On `DELETE`, child tasks are left
    intact by default (they remain usable training data); pass
    `?cascade=true` to also soft-delete the session's child tasks.

    **Status FSM.** `open` (device still streaming / new frames arrived)
    → `infer_in_progress` (an analysis run is in flight) → `inferred`
    (last run succeeded) or `failed` (last run failed). The transitions
    are written by `ai-object-detection-session-analysis` runs; a new
    frame arriving on a settled (`inferred`/`failed`) session reopens it
    to `open` (there is newer data than the last analysis). Per-run
    status/history still lives on the analysis documents. `uploaded` is
    written by the device's upload-complete marker (see the frame intake
    service) — it also fires the category auto-analysis;
    `rearbitrating` remains a reserved/legacy value with no writer.

    **Key relationships.** References `clients` (the `client` /
    store being scanned), `ai-object-detection-category` (the optional
    `category` picked on the device — drives auto-analysis),
    `ai.objectDetectionModelVersions` (via
    `inference_runs.model_version`), `sv.aiObjectDetectionLabels` (via
    `objects.label_id`), and `media.mediaStorages` (point cloud).
    Child frames link back via the task's `session` field.

    **Denormalized aggregates.** `frames_total`, `frames_accepted`,
    `tasks_count` are incremented by the frame intake; `detections_count`
    and `objects_count` are refreshed on every successful analysis run.
    `scene`, `objects[]`, and `inference_runs[]` are **legacy** fields
    from the removed in-session build_scene flow — they are retained for
    older data but no longer written; cross-frame fused output now lives
    in `ai-object-detection-session-analysis`.

    **Population.** `?populatedKeys[]=` supports `client`, `category`,
    `inference_runs.model_version`, `objects.label_id`, and
    `scene.point_cloud_media`. `client`/`category` keep their ids and the
    populated documents are returned under `client_populated` /
    `category_populated`; the nested keys are populated in place (the
    referenced field is replaced by the populated document).
servers:
  - url: https://sv.api.repzo.me
security:
  - ApiKeyAuth: []
  - JwtAuth: []
paths:
  /ai-object-detection-session:
    get:
      summary: Find object detection sessions
      operationId: findAiObjectDetectionSessions
      parameters:
        - in: query
          name: _id
          description: |
            Filter by session `_id`. Pass once for a single match, or as
            `?_id[]=...&_id[]=...` for multiple.
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: session_id
          description: Filter by the device-generated session id (4-char).
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: status
          description: Filter by lifecycle status.
          schema:
            oneOf:
              - type: string
                enum:
                  [
                    open,
                    uploaded,
                    infer_in_progress,
                    inferred,
                    rearbitrating,
                    failed,
                  ]
              - type: array
                items:
                  type: string
                  enum:
                    [
                      open,
                      uploaded,
                      infer_in_progress,
                      inferred,
                      rearbitrating,
                      failed,
                    ]
        - in: query
          name: client
          description: Filter by the scanned store/client (`clients` `_id`).
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: category
          description: Filter by the detection category (`ai-object-detection-category` `_id`).
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: rep
          description: Filter by the rep stamped on the session's frames (`representatives` `_id`).
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: visit_id
          description: Filter by the device visit id the scan happened in.
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: route
          description: Filter by the visit's route (`sv.routes` `_id`).
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: creator._id
          description: Filter by the id of the rep/admin who created the session.
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: inference_runs.model_version
          description: Filter sessions that ran a given model version.
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: from_createdAt
          description: Return sessions created at/after this Unix timestamp (ms).
          schema: { type: number }
        - in: query
          name: to_createdAt
          description: Return sessions created at/before this Unix timestamp (ms).
          schema: { type: number }
        - in: query
          name: from_updatedAt
          description: Cursor — sessions with `updatedAt` greater than this Unix timestamp (ms).
          schema: { type: number }
        - in: query
          name: to_updatedAt
          description: Cursor — sessions with `updatedAt` less than this Unix timestamp (ms).
          schema: { type: number }
        - in: query
          name: disabled
          description: Include disabled (soft-deleted) sessions. Defaults to `false`.
          schema: { type: boolean, default: false }
        - in: query
          name: sort
          description: Field to sort by. Defaults to `_id`.
          schema: { type: string, default: _id }
        - in: query
          name: sortPageOrder
          description: Sort direction. Defaults to descending.
          schema: { type: string, enum: [asc, dsc], default: dsc }
        - in: query
          name: populatedKeys
          description: |
            Embed referenced documents. Supported: `client`, `category`,
            `inference_runs.model_version`, `objects.label_id`,
            `scene.point_cloud_media`.
          schema:
            type: array
            items:
              type: string
              enum:
                - client
                - category
                - inference_runs.model_version
                - objects.label_id
                - scene.point_cloud_media
        - in: query
          name: per_page
          description: Page size. Defaults to the server's configured pagination limit.
          schema: { type: integer, minimum: 1, maximum: 500 }
          example: 50
        - in: query
          name: page
          description: 1-indexed page number.
          schema: { type: integer, minimum: 1 }
          example: 1
      responses:
        "200":
          description: Paginated list of object detection sessions.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionFindResult"
    post:
      summary: Create an object detection session
      description: |
        Opens a session. Usually created implicitly on the first frame
        POST to `/activity-ai-object-detection-session-frame`; this endpoint is
        for explicit/manual creation. `company_namespace` and `creator`
        are server-injected.
      operationId: createAiObjectDetectionSession
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionCreateBody"
      responses:
        "201":
          description: The newly-created session document.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionSchema"
    patch:
      summary: Bulk-update sessions
      description: |
        Bulk update via the `patch-action` writeQuery shape (e.g. bulk
        status change or bulk disable). The rows to update are selected by
        the SAME query filters as `GET /ai-object-detection-session`
        (`_id`, `session_id`, `status`, `client`, `category`, `rep`,
        `visit_id`, `route`, `creator._id`, `inference_runs.model_version`,
        `from_/to_createdAt`, `from_/to_updatedAt`, `disabled`). Returns
        `{ nFound, nModified }`.
      operationId: patchAiObjectDetectionSession
      parameters:
        - in: query
          name: _id
          description: Restrict the bulk update to these session ids.
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: session_id
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: status
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: client
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: category
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: rep
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: visit_id
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: route
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: creator._id
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: inference_runs.model_version
          schema:
            oneOf:
              - type: string
              - type: array
                items: { type: string }
        - in: query
          name: from_createdAt
          schema: { type: number }
        - in: query
          name: to_createdAt
          schema: { type: number }
        - in: query
          name: from_updatedAt
          schema: { type: number }
        - in: query
          name: to_updatedAt
          schema: { type: number }
        - in: query
          name: disabled
          schema: { type: boolean }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PatchActionBody"
      responses:
        "200":
          description: Bulk-update result.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PatchActionResult"
  /ai-object-detection-session/{id}:
    get:
      summary: Get a session by id
      operationId: getAiObjectDetectionSession
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: query
          name: populatedKeys
          description: Same population keys as the list endpoint.
          schema:
            type: array
            items:
              type: string
              enum:
                - client
                - category
                - inference_runs.model_version
                - objects.label_id
                - scene.point_cloud_media
      responses:
        "200":
          description: The session document for the given `_id`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionSchema"
    put:
      summary: Update a session
      description: |
        Standard put — typically for limited fields (status corrections,
        capture metadata). `company_namespace` is derived from the
        caller's session.
      operationId: updateAiObjectDetectionSession
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SessionUpdateBody"
      responses:
        "200":
          description: The session document after the update is applied.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionSchema"
    delete:
      summary: Soft-delete a session
      description: |
        Soft-deletes the session (`disabled: true`). By default child
        tasks are kept (usable training data). Pass `?cascade=true` to
        also soft-delete the session's child tasks.
      operationId: removeAiObjectDetectionSession
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string }
        - in: query
          name: cascade
          description: When `true`, also soft-delete the session's child tasks.
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: "The session document after soft-deletion (`disabled: true`)."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionSchema"
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: api-key
      description: |
        Server-issued API key. Also accepted via the `x-api-key` header or the
        `?apiKey=` query parameter as fallbacks.
    JwtAuth:
      type: apiKey
      in: header
      name: Authorization
      description: |
        Raw JWT in the `Authorization` header — **no `Bearer ` prefix**.
        Obtained from `POST /authenticate` (admin / rep / client login).
  schemas:
    UserRef:
      type: object
      description: Compact actor reference (rep or admin).
      properties:
        _id: { type: string }
        type: { type: string, enum: [admin, rep] }
        name: { type: string }
        rep: { type: string }
        admin: { type: string }
    Device:
      type: object
      properties:
        platform: { type: string }
        os: { type: string }
        model: { type: string }
        app_version: { type: string }
        ar_engine: { type: string, enum: [ARKit, ARCore] }
    CaptureSettings:
      type: object
      properties:
        rate_hz: { type: number }
        recording_enabled: { type: boolean }
        target_distance_m: { type: number }
        resolution:
          type: string
          enum: [medium, high, max]
          description: Camera format preference active during capture.
    Plane:
      type: object
      properties:
        kind: { type: string, enum: [shelf, floor, wall] }
        normal:
          type: array
          items: { type: number }
          description: 3-float plane normal.
        d:
          type: number
          description: "Plane equation constant in Ax+By+Cz+d=0."
        inliers: { type: number }
        bounds:
          type: object
          properties:
            min: { type: array, items: { type: number } }
            max: { type: array, items: { type: number } }
    SessionObject:
      type: object
      description: Cross-frame fused object after clustering.
      properties:
        _id: { type: string }
        label_id: { type: string }
        cluster_size:
          type: number
          description: How many frames contributed to this object.
        world_position:
          type: object
          properties:
            x: { type: number }
            y: { type: number }
            z: { type: number }
        world_orientation:
          type: object
          properties:
            yaw: { type: number }
            pitch: { type: number }
            roll: { type: number }
        bbox_3d:
          type: object
          properties:
            min: { type: array, items: { type: number } }
            max: { type: array, items: { type: number } }
        placement_confidence:
          type: number
          description: Fused 0..1 placement confidence.
        winning_task: { type: string }
        winning_annotation_id: { type: string }
        contributing_tasks:
          type: array
          items: { type: string }
        arbitration:
          type: object
          additionalProperties: true
          description: Weights / tiebreak rationale.
    InferenceRun:
      type: object
      properties:
        _id: { type: string }
        model_version: { type: string }
        started_at: { type: number }
        finished_at: { type: number }
        config:
          type: object
          additionalProperties: true
          description: Clustering params, RANSAC config.
        status: { type: string, enum: [pending, success, failed] }
        triggered_by:
          $ref: "#/components/schemas/UserRef"
        notes: { type: string }
    Scene:
      type: object
      properties:
        planes:
          type: array
          items:
            $ref: "#/components/schemas/Plane"
        point_cloud_media:
          type: string
          description: "`media.mediaStorages` id of the .ply / .npy point cloud."
        point_cloud_stats:
          type: object
          properties:
            n_points: { type: number }
            voxel_size_m: { type: number }
            bbox_min: { type: array, items: { type: number } }
            bbox_max: { type: array, items: { type: number } }
        world_frame:
          type: string
          enum: [arkit, arcore, normalized]
          description: Coordinate system the world positions are reported in.
    SessionSchema:
      type: object
      description: Object detection session document.
      properties:
        _id: { type: string }
        session_id:
          type: string
          description: "Device-generated session id (4-char). Election clones carry a `-E<n>` suffix."
        source_session:
          type: string
          description: Provenance — the session this one was materialized from by `ai-object-detection-session-election` (absent on device-captured sessions).
        device:
          $ref: "#/components/schemas/Device"
        client:
          type: string
          description: "`clients` `_id` of the scanned store/client (optional)."
        client_populated:
          type: object
          description: Populated client document (when `client` is in `populatedKeys`).
          additionalProperties: true
        category:
          type: string
          description: |
            `ai-object-detection-category` `_id` picked on the device before
            calibration (optional). Drives the auto-analysis fired when the
            device posts the upload-complete marker.
        category_populated:
          type: object
          description: Populated category document (when `category` is in `populatedKeys`).
          additionalProperties: true
        mission:
          type: string
          description: |
            `ai-object-detection-mission` `_id` the session was STARTED FROM
            on the device ("SCAN THIS MISSION") — optional; generic scans
            carry none. The attribution anchor: mission completions and
            mission-result rows credit THIS mission only.
        rep:
          type: string
          description: "`representatives` `_id` sent on the frames (defaults to the rep in the token) — the scanning rep for api-key intakes."
        visit_id:
          type: string
          description: |
            Device visit id (`visits.visit_id`) of the visit the scan happened
            in — stamped by the frame intake, stored as-is (visits sync after
            the fact, never resolved to a server ref). Copied onto the
            mission/metric results so the assigned-missions read can tell
            "done in THIS visit".
        route:
          type: string
          description: "`sv.routes` `_id` of the visit's route, as sent by the device."
        business_day:
          type: string
          description: "Business day of the scan, `YYYY-MM-DD` — as sent by the device, else stamped once from the capture time under the rep's stamping context (shift schedule, `end_of_day` cut, time zone)."
        time_zone:
          type: string
          description: IANA timezone of the device at capture, as sent by the device.
        aisle_label: { type: string }
        aisle_length_m: { type: number }
        capture_settings:
          $ref: "#/components/schemas/CaptureSettings"
        status:
          type: string
          enum:
            [open, uploaded, infer_in_progress, inferred, rearbitrating, failed]
        _errors:
          type: array
          items: { type: object, additionalProperties: true }
        frames_total: { type: number }
        frames_accepted: { type: number }
        tasks_count: { type: number }
        detections_count: { type: number }
        objects_count: { type: number }
        ground:
          type: object
          description: |
            Floor lock measured on-device at capture entry (absent when the
            rep skipped it). World Y is gravity-aligned, so the height above
            the floor of any world point is "y - ground.y_world".
          properties:
            y_world:
              { type: number, description: Floor height (world Y), metres. }
            samples: { type: integer }
            spread_m:
              {
                type: number,
                description: Lock quality (IQR of the samples),
                metres.,
              }
            camera_height_m: { type: number }
        eye_level_m:
          type: number
          description: |
            Camera height above the locked floor while shooting the shelf,
            metres — the rep's standing eye level.
        world_alignment:
          type: string
          description: "'gravity' on both ARKit/ARCore — recorded, not assumed."
        coverage_m2:
          type: number
          description: Device-computed swept shelf area, m² — includes quality-rejected (never uploaded) attempts, so it cannot be recomputed server-side. Monotonic per session.
        ground_skipped:
          type: boolean
          description: The rep tapped Skip on the floor point (a deliberate choice, unlike a lock that never converged).
        session_score:
          type: number
          description: "Election evaluation — average frame quality, 0..1 (written by `ai-object-detection-session-election`)."
        session_verdict:
          type: string
          enum: [excellent, good, acceptable, rejected]
          description: Articulated verdict from `session_score` against the namespace detection-settings score bands; `rejected` when any rejection reason fired.
        rejection_reasons:
          type: array
          items: { type: string }
          description: "Session gates that fired at election: `coverage_below_target`, `jump_detected`, `too_many_elected`."
        election_excluded:
          type: array
          description: Error-tier frames excluded from the election (they stay on the source session).
          items:
            type: object
            properties:
              task: { type: string }
              violations:
                type: array
                items: { type: string }
        scene:
          $ref: "#/components/schemas/Scene"
        objects:
          type: array
          items:
            $ref: "#/components/schemas/SessionObject"
        inference_runs:
          type: array
          items:
            $ref: "#/components/schemas/InferenceRun"
        creator:
          $ref: "#/components/schemas/UserRef"
        editor:
          $ref: "#/components/schemas/UserRef"
        disabled: { type: boolean }
        company_namespace:
          type: array
          items: { type: string }
          description: Tenant key. Server-injected — never accept from clients.
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    SessionCreateBody:
      type: object
      description: |
        Body for opening a session. `creator` and the aggregate counters are
        server-managed. The tenant key (`company_namespace`) is optional for
        SDK callers and is otherwise injected from the caller's session.
      properties:
        session_id: { type: string }
        source_session: { type: string }
        device:
          $ref: "#/components/schemas/Device"
        client: { type: string }
        category: { type: string }
        mission: { type: string }
        rep: { type: string }
        visit_id: { type: string }
        route: { type: string }
        business_day:
          type: string
          description: "`YYYY-MM-DD`."
        time_zone: { type: string }
        capture_settings:
          $ref: "#/components/schemas/CaptureSettings"
        coverage_m2: { type: number }
        ground:
          type: object
          properties:
            y_world: { type: number }
            samples: { type: integer }
            spread_m: { type: number }
            camera_height_m: { type: number }
        eye_level_m: { type: number }
        ground_skipped: { type: boolean }
        world_alignment: { type: string }
        status:
          type: string
          enum:
            [open, uploaded, infer_in_progress, inferred, rearbitrating, failed]
          default: open
        company_namespace:
          type: array
          items: { type: string }
          description: Optional tenant namespace override for SDK callers.
    SessionUpdateBody:
      type: object
      description: |
        Body for updating a session (any stored field; `editor` is
        server-stamped). The tenant key is derived from the caller's
        session. Set `disabled: true` to soft-delete.
      properties:
        session_id: { type: string }
        device:
          $ref: "#/components/schemas/Device"
        client: { type: string }
        category: { type: string }
        mission: { type: string }
        rep: { type: string }
        visit_id: { type: string }
        route: { type: string }
        business_day: { type: string }
        time_zone: { type: string }
        capture_settings:
          $ref: "#/components/schemas/CaptureSettings"
        coverage_m2: { type: number }
        ground:
          type: object
          properties:
            y_world: { type: number }
            samples: { type: integer }
            spread_m: { type: number }
            camera_height_m: { type: number }
        eye_level_m: { type: number }
        ground_skipped: { type: boolean }
        world_alignment: { type: string }
        status:
          type: string
          enum:
            [open, uploaded, infer_in_progress, inferred, rearbitrating, failed]
        disabled: { type: boolean }
    PatchActionWrite:
      type: object
      properties:
        key: { type: string }
        command:
          type: string
          enum: [set, addToSet, pull]
        value: {}
    PatchActionBody:
      type: object
      description: Bulk-update body. `writeQuery[]` describes the writes applied to filtered rows.
      properties:
        writeQuery:
          type: array
          items:
            $ref: "#/components/schemas/PatchActionWrite"
    PatchActionResult:
      type: object
      properties:
        nFound: { type: number }
        nModified: { type: number }
    SessionFindResult:
      type: object
      description: Standard paginated result envelope.
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/SessionSchema"
        total_result: { type: number }
        current_count: { type: number }
        total_pages: { type: number }
        current_page: { type: number }
        per_page: { type: number }
        first_page_url: { type: string }
        last_page_url: { type: string }
        next_page_url: { type: string, nullable: true }
        prev_page_url: { type: string, nullable: true }
        path: { type: string }
