openapi: 3.1.0
info:
  title: UTL API
  version: 0.1.0-draft
  description: |
    Utility Service (UTL) tenant offboarding and export-only snapshots.

    Contract notes:
    - API Gateway base path: `/utl`
    - Offboarding request stage is API + direct Lambda; export/purge/archive flows are direct Lambda only.
    - Export-only is owner-initiated via API Gateway (no freeze).
    - Auth: `session_guid` is required in the request body for tenant API calls.
    - Org-gated: `orgcode` is required in the request body.
servers:
  - url: https://api.g3nretailstack.com/utl
security:
  - sessionAuth: []
components:
  securitySchemes:
    sessionAuth:
      type: apiKey
      in: header
      name: x-session-guid
  schemas:
    BuildMeta:
      type: object
      properties:
        build_major: { type: string }
        build_minor: { type: string }
        build_id: { type: string }
      required: [build_major, build_minor, build_id]
    Stats:
      type: object
      properties:
        call: { type: string }
        service: { type: string, enum: [utl] }
        request_id: { type: string }
        timestamp_utc: { type: string, format: date-time }
        actor: { type: string }
        context_source:
          type: string
          enum: [session, operator, system]
        session_fingerprint:
          type: string
          description: Non-reversible SHA-256 fingerprint of the caller session for correlation.
        orgcode: { type: string }
        cccode: { type: string }
        build:
          $ref: '#/components/schemas/BuildMeta'
      required: [call, service, timestamp_utc, build]
    ErrorMajor:
      type: object
      properties:
        tag: { type: string }
        message:
          type: object
          properties:
            en_US: { type: string }
      required: [tag, message]
    ErrorEnvelope:
      type: object
      properties:
        error_code: { type: string }
        http_status: { type: integer }
        retryable: { type: boolean }
        request_id: { type: string }
        trace_id: { type: string }
        major: { $ref: '#/components/schemas/ErrorMajor' }
        minor: { $ref: '#/components/schemas/ErrorMajor' }
        details: { type: object }
        conflict_snapshot: { type: object }
      required: [major]
    Envelope:
      type: object
      properties:
        success: { type: boolean }
        data: { type: object }
        error: { $ref: '#/components/schemas/ErrorEnvelope' }
        stats: { $ref: '#/components/schemas/Stats' }
        revision: { type: string }
      required: [success]
    ExportManifest:
      type: object
      properties:
        bucket: { type: string }
        key: { type: string }
      required: [bucket, key]
    OffboardingStatus:
      type: string
      enum:
        - requested
        - canceled
        - approved
        - export_window_open
        - exporting
        - exported
        - purge_pending
        - purged
        - archived
        - archived_deleted
        - archive_restore_pending
        - archive_restored
    OffboardingRecord:
      type: object
      properties:
        entity_type: { type: string }
        orgcode: { type: string }
        org_guid: { type: string }
        org_caption: { type: string }
        org_legal_name: { type: string }
        owner_user_guids:
          type: array
          items: { type: string }
        request_id: { type: string }
        run_id: { type: string }
        status: { $ref: '#/components/schemas/OffboardingStatus' }
        requested_by_user_guid: { type: string }
        approved_by: { type: string }
        requested_export_at: { type: string, format: date-time }
        latest_start_at: { type: string, format: date-time }
        export_window_opened_at: { type: string, format: date-time }
        export_started_at: { type: string, format: date-time }
        export_completed_at: { type: string, format: date-time }
        export_expires_at: { type: string, format: date-time }
        purge_started_at: { type: string, format: date-time }
        purge_completed_at: { type: string, format: date-time }
        archived_at: { type: string, format: date-time }
        archive_deleted_at: { type: string, format: date-time }
        archive_restore_requested_at: { type: string, format: date-time }
        archive_restore_completed_at: { type: string, format: date-time }
        legal_hold: { type: boolean }
        legal_hold_reason: { type: string }
        legal_hold_case_ref: { type: string }
        legal_hold_requested_by: { type: string }
        legal_hold_approved_by: { type: string }
        legal_hold_exception_reason: { type: string }
        legal_hold_set_at: { type: string, format: date-time }
        legal_hold_cleared_at: { type: string, format: date-time }
        format_requested: { type: string, enum: [jsonl, parquet] }
        format_final: { type: string, enum: [jsonl, parquet] }
        export_manifest: { $ref: '#/components/schemas/ExportManifest' }
        export_stats_summary: { type: object }
        purge_stats_summary: { type: object }
        purge_verification_status: { type: string, enum: [pending, passed, failed] }
        purge_verified_at: { type: string, format: date-time }
        purge_verification_report:
          type: object
          properties:
            bucket: { type: string }
            key: { type: string }
        cost_summary: { type: object }
        notification_log:
          type: array
          items:
            type: object
            properties:
              event: { type: string }
              channel: { type: string }
              target: { type: string }
              status: { type: string }
              sent_at: { type: string, format: date-time }
              delivery_id: { type: string }
        status_history:
          type: array
          items:
            type: object
            properties:
              status: { type: string }
              at: { type: string, format: date-time }
              reason: { type: string }
              actor: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        revision: { type: string }
      additionalProperties: true
    OffboardingRequest:
      type: object
      properties:
        orgcode: { type: string }
        session_guid: { type: string }
        requested_export_at: { type: string, format: date-time }
        format_preference: { type: string, enum: [jsonl, parquet] }
        expected_revision: { type: string }
        reason: { type: string }
      required: [orgcode, session_guid, requested_export_at, reason]
    OffboardingCancelRequest:
      type: object
      properties:
        orgcode: { type: string }
        session_guid: { type: string }
        expected_revision: { type: string }
        reason: { type: string }
      required: [orgcode, session_guid, expected_revision, reason]
    OffboardingStatusRequest:
      type: object
      properties:
        orgcode: { type: string }
        session_guid: { type: string }
      required: [orgcode, session_guid]
    OffboardingStatusResponse:
      type: object
      properties:
        offboarding: { $ref: '#/components/schemas/OffboardingRecord' }
        export_manifest: { $ref: '#/components/schemas/ExportManifest' }
        export_location:
          type: object
          properties:
            bucket: { type: string }
            prefix: { type: string }
    ExportOnlyStatus:
      type: string
      enum: [requested, exporting, exported, failed, canceled]
    ExportOnlyProgress:
      type: object
      properties:
        completed_services: { type: integer }
        service_total: { type: integer }
        percent: { type: integer }
        current_service: { type: string }
    ExportOnlyRecord:
      type: object
      properties:
        entity_type: { type: string }
        orgcode: { type: string }
        org_guid: { type: string }
        org_caption: { type: string }
        org_legal_name: { type: string }
        export_id: { type: string }
        run_id: { type: string }
        status: { $ref: '#/components/schemas/ExportOnlyStatus' }
        requested_by_user_guid: { type: string }
        requested_at: { type: string, format: date-time }
        export_started_at: { type: string, format: date-time }
        export_completed_at: { type: string, format: date-time }
        export_expires_at: { type: string, format: date-time }
        format_requested: { type: string, enum: [jsonl, parquet] }
        format_final: { type: string, enum: [jsonl, parquet] }
        export_manifest: { $ref: '#/components/schemas/ExportManifest' }
        export_stats_summary: { type: object }
        cost_summary: { type: object }
        progress: { $ref: '#/components/schemas/ExportOnlyProgress' }
        error:
          type: object
          properties:
            message: { type: string }
            code: { type: string }
            retryable: { type: boolean }
            at: { type: string, format: date-time }
            detail: { type: object }
        status_history:
          type: array
          items:
            type: object
            properties:
              status: { type: string }
              at: { type: string, format: date-time }
              reason: { type: string }
              actor: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        revision: { type: string }
      additionalProperties: true
    ExportOnlyRequest:
      type: object
      properties:
        orgcode: { type: string }
        session_guid: { type: string }
        format_preference: { type: string, enum: [jsonl, parquet] }
        reason: { type: string }
      required: [orgcode, session_guid, reason]
    ExportOnlyStatusRequest:
      type: object
      properties:
        orgcode: { type: string }
        session_guid: { type: string }
        export_id: { type: string }
      required: [orgcode, session_guid, export_id]
    ExportOnlyDownloadStartRequest:
      type: object
      properties:
        orgcode: { type: string }
        session_guid: { type: string }
        export_id: { type: string }
      required: [orgcode, session_guid, export_id]
    ExportOnlyStatusResponse:
      type: object
      properties:
        export: { $ref: '#/components/schemas/ExportOnlyRecord' }
        export_manifest: { $ref: '#/components/schemas/ExportManifest' }
        export_location:
          type: object
          properties:
            bucket: { type: string }
            prefix: { type: string }
    ExportOnlyDownloadStartResponse:
      type: object
      properties:
        export_id: { type: string }
        export_manifest: { $ref: '#/components/schemas/ExportManifest' }
        export_location:
          type: object
          properties:
            bucket: { type: string }
            prefix: { type: string }
        download:
          type: object
          properties:
            expires_in_seconds: { type: integer }
            manifest_url: { type: string }
            service_manifest_urls:
              type: object
              additionalProperties: { type: string }
paths:
  /stat:
    get:
      summary: Health check
      description: |
        Health check. UTL tenant calls require session_guid in the JSON body (header auth not used).
        Org-scoped reads may return 404 for non-associated callers (anti-enumeration). Route class Tier A
        (p95 500ms).
      x-route-class: Tier A
      x-qps-target: 5
      x-concurrency-target: 2
      x-latency-p95-ms: 500
      responses:
        '200':
          description: ok
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          ok: { type: boolean }
  /offboarding/request:
    post:
      summary: Create offboarding request
      description: |
        Create offboarding request. UTL tenant calls require session_guid in the JSON body (header auth not
        used). Route class Tier D (p95 2000ms, p99 4000ms).
      x-route-class: Tier D
      x-qps-target: 10
      x-concurrency-target: 5
      x-latency-p95-ms: 2000
      x-latency-p99-ms: 4000
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/OffboardingRequest' }
      responses:
        '200':
          description: Offboarding request created
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          offboarding: { $ref: '#/components/schemas/OffboardingRecord' }
  /offboarding/cancel:
    post:
      summary: Cancel offboarding request
      description: |
        Cancel offboarding request. UTL tenant calls require session_guid in the JSON body (header auth not
        used). Route class Tier D (p95 2000ms, p99 4000ms).
      x-route-class: Tier D
      x-qps-target: 10
      x-concurrency-target: 5
      x-latency-p95-ms: 2000
      x-latency-p99-ms: 4000
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/OffboardingCancelRequest' }
      responses:
        '200':
          description: Offboarding request canceled
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          offboarding: { $ref: '#/components/schemas/OffboardingRecord' }
  /offboarding/status:
    post:
      summary: Get offboarding request status
      description: |
        Get offboarding request status. UTL tenant calls require session_guid in the JSON body (header auth
        not used). Org-scoped reads may return 404 for non-associated callers (anti-enumeration). If this
        updates a revisioned record, expected_revision is required (428 if missing; 409 on mismatch). Route
        class Tier B (p95 400ms).
      x-route-class: Tier B
      x-qps-target: 30
      x-concurrency-target: 10
      x-latency-p95-ms: 400
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/OffboardingStatusRequest' }
      responses:
        '200':
          description: Offboarding request status
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/OffboardingStatusResponse' }
  /export/request:
    post:
      summary: Create export-only request
      description: |
        Create export-only request. UTL tenant calls require session_guid in the JSON body (header auth not
        used). Route class Tier C (p95 1500ms, p99 3000ms).
      x-route-class: Tier C
      x-qps-target: 10
      x-concurrency-target: 5
      x-latency-p95-ms: 1500
      x-latency-p99-ms: 3000
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ExportOnlyRequest' }
      responses:
        '200':
          description: Export-only request created
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          export: { $ref: '#/components/schemas/ExportOnlyRecord' }
  /export/status:
    post:
      summary: Get export-only status
      description: |
        Get export-only status. UTL tenant calls require session_guid in the JSON body (header auth not
        used). Org-scoped reads may return 404 for non-associated callers (anti-enumeration). If this
        updates a revisioned record, expected_revision is required (428 if missing; 409 on mismatch). Route
        class Tier B (p95 400ms).
      x-route-class: Tier B
      x-qps-target: 30
      x-concurrency-target: 10
      x-latency-p95-ms: 400
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ExportOnlyStatusRequest' }
      responses:
        '200':
          description: Export-only status
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/ExportOnlyStatusResponse' }
  /export/download/start:
    post:
      summary: Start export-only download
      description: |
        Start export-only download. UTL tenant calls require session_guid in the JSON body (header auth not
        used). Route class Tier C (p95 1500ms, p99 3000ms).
      x-route-class: Tier C
      x-qps-target: 15
      x-concurrency-target: 5
      x-latency-p95-ms: 1500
      x-latency-p99-ms: 3000
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ExportOnlyDownloadStartRequest' }
      responses:
        '200':
          description: Download URLs for export artifacts
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: '#/components/schemas/Envelope' }
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/ExportOnlyDownloadStartResponse' }
