openapi: 3.0.0
info:
  title: Olympus Oncall API
  description: API for Olympus Oncall Shift Management System
  version: 2.0.0
servers:
  - url: /
    description: Relative API base
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
security:
  - bearerAuth: []
paths:
  /api/v1/schedule:
    get:
      operationId: getSchedule
      summary: Effective schedule rows by team_ids and time range
      parameters:
        - in: query
          name: team_ids
          schema:
            type: array
            items:
              type: integer
          description: Repeatable, e.g. ?team_ids=1&team_ids=2
        - in: query
          name: team_ids_csv
          schema:
            type: string
        - in: query
          name: team_id
          schema:
            type: integer
        - in: query
          name: start_time
          schema:
            type: integer
        - in: query
          name: end_time
          schema:
            type: integer
        - in: query
          name: is_backup
          schema:
            type: boolean
        - in: query
          name: include_empty
          description: Also emit engineer_id=0 rows for periods with no one on call
          schema:
            type: boolean
      responses:
        '200':
          description: Effective overlay schedule rows
        '400':
          description: Bad Request
  /api/v1/schedule/overview:
    get:
      operationId: getScheduleOverview
      summary: Current on-call overview
      parameters:
        - in: query
          name: group_name
          schema:
            type: string
      responses:
        '200':
          description: OK
  /api/v1/teams:
    get:
      operationId: listTeams
      summary: List teams
      parameters:
        - in: query
          name: group_name
          schema:
            type: string
      responses:
        '200':
          description: OK
    post:
      operationId: createTeam
      summary: Create a team
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                display_name:
                  type: string
                managers:
                  type: array
                  items:
                    type: string
                group_names:
                  type: array
                  items:
                    type: string
                sort_priority:
                  type: integer
                description:
                  type: string
                validate_feishu:
                  type: boolean
                validate_domain:
                  type: boolean
      responses:
        '200':
          description: OK
        '403':
          description: Forbidden
  /api/v1/teams/{ref}:
    parameters:
      - in: path
        name: ref
        required: true
        schema:
          type: string
        description: Numeric team id or unique slug name
    get:
      operationId: getTeam
      summary: Get team detail (by id or slug)
      description: >
        Returns the caller's standing in the team: role (member/editor/manager,
        empty for a non-member), can_write (editor or manager) and is_manager.
      responses:
        '200':
          description: OK
        '404':
          description: Not Found
    patch:
      operationId: updateTeam
      summary: Update team base config
      description: >
        Editors and managers may change the descriptive fields. The managers
        list replaces the team's whole manager set, so a request carrying it is
        manager-only.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                display_name:
                  type: string
                group_names:
                  type: array
                  items:
                    type: string
                sort_priority:
                  type: integer
                managers:
                  type: array
                  description: Replaces the manager set. Manager-only.
                  items:
                    type: string
                description:
                  type: string
                feishu_chat_id:
                  type: string
                validate_feishu:
                  type: boolean
                validate_domain:
                  type: boolean
      responses:
        '200':
          description: OK
        '403':
          description: Forbidden
    delete:
      operationId: disableTeam
      summary: Disable (soft-delete) a team
      description: Manager-only.
      responses:
        '200':
          description: OK
        '403':
          description: Forbidden
  /api/v1/teams/{ref}/chat:
    parameters:
      - in: path
        name: ref
        required: true
        schema:
          type: string
        description: Numeric team id or unique slug name
    post:
      operationId: createTeamChat
      summary: Create a Feishu group chat for a team
      description: >
        Creates a new Feishu group chat named "Oncall – {display_name}", adds
        all current team members, and saves the chat_id to the team record.
      responses:
        '200':
          description: OK
        '403':
          description: Forbidden
  /api/v1/teams/{ref}/current-oncall:
    get:
      operationId: getTeamCurrentOncall
      summary: Current on-call engineer for a team
      parameters:
        - in: path
          name: ref
          required: true
          schema:
            type: string
        - in: query
          name: at_time
          schema:
            type: integer
      responses:
        '200':
          description: OK
  /api/v1/teams/{ref}/schedule/computed:
    get:
      operationId: getTeamComputedSchedule
      summary: Computed schedule window for a team
      parameters:
        - in: path
          name: ref
          required: true
          schema:
            type: string
        - in: query
          name: from
          schema:
            type: integer
        - in: query
          name: to
          schema:
            type: integer
      responses:
        '200':
          description: OK
  /api/v1/teams/{ref}/schedule/preview:
    post:
      operationId: previewTeamSchedule
      summary: Dry-run preview of a manual segment change
      parameters:
        - in: path
          name: ref
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                start_time:
                  type: integer
                end_time:
                  type: integer
                engineer_email:
                  type: string
                is_backup:
                  type: boolean
      responses:
        '200':
          description: OK
  /api/v1/teams/{ref}/layers:
    parameters:
      - in: path
        name: ref
        required: true
        schema:
          type: string
    get:
      operationId: getTeamLayers
      summary: Get team rotation layers
      responses:
        '200':
          description: OK
    put:
      operationId: replaceTeamLayers
      summary: Replace team rotation layers
      description: >
        Declarative replace: items carrying an existing layer id are updated in
        place (their handover grid carries over), the rest are created, and
        unreferenced layers are deleted. The rotation phase is derived from the
        optional handoff_at / next_up_email fields, never set directly.
        A people entry of "placeholder" (or "placeholder:<label>") is an empty
        slot: it takes its turn in the rotation with nobody on call.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                layers:
                  type: array
                  items:
                    type: object
      responses:
        '200':
          description: OK
        '403':
          description: Forbidden
  /api/v1/teams/{ref}/layers/preview:
    post:
      operationId: previewTeamLayers
      summary: Dry-run a rotation layers change (nothing written)
      parameters:
        - in: path
          name: ref
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                layers:
                  type: array
                  items:
                    type: object
      responses:
        '200':
          description: OK
        '403':
          description: Forbidden
  /api/v1/teams/{ref}/backup-lane:
    put:
      operationId: setTeamBackupLane
      summary: Enable/disable the backup lane
      parameters:
        - in: path
          name: ref
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                enabled:
                  type: boolean
      responses:
        '200':
          description: OK
        '403':
          description: Forbidden
  /api/v1/teams/{ref}/available-engineers:
    get:
      operationId: getTeamAvailableEngineers
      summary: Engineers available to add to a team
      parameters:
        - in: path
          name: ref
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
  /api/v1/teams/{ref}/engineers:
    post:
      operationId: addTeamEngineer
      summary: Add an engineer to a team
      parameters:
        - in: path
          name: ref
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                engineer_id:
                  type: integer
                engineer_email:
                  type: string
      responses:
        '200':
          description: OK
        '403':
          description: Forbidden
  /api/v1/teams/{ref}/engineers/{eref}:
    delete:
      operationId: removeTeamEngineer
      summary: Remove an engineer from a team (by id or email)
      description: >
        Editors may only remove plain members — removing an editor or a manager
        requires a manager, since dropping the membership also drops the role.
      parameters:
        - in: path
          name: ref
          required: true
          schema:
            type: string
        - in: path
          name: eref
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
        '403':
          description: Forbidden
  /api/v1/teams/{ref}/engineers/{eref}/role:
    put:
      operationId: setTeamEngineerRole
      summary: Set a team member's role
      description: >
        Moves a member along the team's permission ladder: member (read-only,
        may still take an on-call slot themselves) < editor (may change the
        schedule and add or remove plain members) < manager (may also hand out
        roles and disable the team). Manager-only; a team's last manager cannot
        be demoted.
      parameters:
        - in: path
          name: ref
          required: true
          schema:
            type: string
        - in: path
          name: eref
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - role
              properties:
                role:
                  type: string
                  enum: [member, editor, manager]
      responses:
        '200':
          description: OK
        '400':
          description: Invalid role, or the change would leave the team without a manager
        '403':
          description: Forbidden
  /api/v1/teams/{ref}/managers:
    parameters:
      - in: path
        name: ref
        required: true
        schema:
          type: string
    get:
      operationId: getTeamManagers
      summary: List team managers
      responses:
        '200':
          description: OK
    post:
      operationId: addTeamManager
      summary: Add a manager to a team
      description: Manager-only. Shorthand for setTeamEngineerRole with role=manager.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                engineer_id:
                  type: integer
                engineer_email:
                  type: string
      responses:
        '200':
          description: OK
        '403':
          description: Forbidden
  /api/v1/teams/{ref}/managers/{email}:
    delete:
      operationId: removeTeamManager
      summary: Remove a manager from a team
      description: >
        Manager-only. Demotes them to a plain member rather than removing them
        from the team; a team's last manager cannot be removed.
      parameters:
        - in: path
          name: ref
          required: true
          schema:
            type: string
        - in: path
          name: email
          required: true
          schema:
            type: string
      responses:
        '200':
          description: OK
        '403':
          description: Forbidden
  /api/v1/notifications/feishu:
    post:
      operationId: sendFeishuNotification
      summary: Send the Feishu on-call notification
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                group_name:
                  type: string
                at_time:
                  type: integer
                skip_same_substitute:
                  type: boolean
                exclude_test_teams:
                  type: boolean
                recipients:
                  type: array
                  items:
                    type: string
                recipient_emails:
                  type: array
                  items:
                    type: string
      responses:
        '200':
          description: OK
  /api/v1/notifications/feishu/preview:
    post:
      operationId: previewFeishuNotification
      summary: Preview the Feishu on-call notification
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                group_name:
                  type: string
                at_time:
                  type: integer
                skip_same_substitute:
                  type: boolean
                exclude_test_teams:
                  type: boolean
      responses:
        '200':
          description: OK
  /api/v1/notifications/private:
    post:
      operationId: sendPrivateNotification
      summary: Send the privilege/private batch (admin only)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                group_name:
                  type: string
                at_time:
                  type: integer
                exclude_test_teams:
                  type: boolean
      responses:
        '200':
          description: OK
        '403':
          description: Forbidden
  /api/v1/notifications/private/preview:
    post:
      operationId: previewPrivateNotification
      summary: Preview the privilege/private batch (admin only)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                group_name:
                  type: string
                at_time:
                  type: integer
                exclude_test_teams:
                  type: boolean
      responses:
        '200':
          description: OK
        '403':
          description: Forbidden
  /api/v1/overlays:
    post:
      operationId: applyOverlay
      summary: Apply manual overrides (segment for one team, or items across teams)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                type:
                  type: string
                  description: segment | items
                team_id:
                  type: integer
                plan_signature:
                  type: string
                start_time:
                  type: integer
                end_time:
                  type: integer
                engineer_email:
                  type: string
                is_backup:
                  type: boolean
                items:
                  type: array
                  items:
                    type: object
      responses:
        '200':
          description: OK
        '403':
          description: Forbidden
    delete:
      operationId: removeOverlay
      summary: Remove manual overrides in a time range (revert to baseline)
      parameters:
        - in: query
          name: team_id
          required: true
          schema:
            type: integer
        - in: query
          name: start_time
          schema:
            type: integer
        - in: query
          name: end_time
          schema:
            type: integer
        - in: query
          name: is_backup
          schema:
            type: boolean
      responses:
        '200':
          description: OK
        '403':
          description: Forbidden
  /api/v1/system-phones:
    get:
      operationId: getSystemPhones
      summary: System phone numbers (caller IDs oncallers should whitelist)
      responses:
        '200':
          description: OK
        '401':
          description: Unauthorized
  /api/v1/me/page-phone:
    get:
      operationId: getPagePhone
      summary: Get current user's page phone override
      responses:
        '200':
          description: OK
        '401':
          description: Unauthorized
        '404':
          description: Engineer not found
    put:
      operationId: updatePagePhone
      summary: Set or clear page phone override
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                page_phone:
                  type: string
      responses:
        '200':
          description: OK
        '404':
          description: Engineer not found
  /api/v1/page:
    post:
      operationId: testPage
      summary: Page a user (Feishu urgent + phone call)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - user_email
              properties:
                user_email:
                  type: string
                reason:
                  type: string
      responses:
        '200':
          description: OK
        '400':
          description: Bad Request
  /api/v1/api-keys:
    get:
      operationId: listApiKeys
      summary: List API keys
      responses:
        '200':
          description: OK
    post:
      operationId: createApiKey
      summary: Create a user API key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
      responses:
        '200':
          description: OK
  /api/v1/api-keys/service:
    post:
      operationId: createServiceApiKey
      summary: Create a service (machine-to-machine) API key (admin only)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                scopes:
                  type: array
                  items:
                    type: string
      responses:
        '200':
          description: OK
        '403':
          description: Forbidden
  /api/v1/api-keys/{id}:
    delete:
      operationId: deleteApiKey
      summary: Delete an API key
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: OK
        '403':
          description: Forbidden
  /api/v1/me:
    get:
      operationId: getMe
      summary: Current user identity and RBAC flags
      description: >
        managed_team_ids are the teams the caller manages, writable_team_ids
        the ones they may edit (managed teams included) and read_team_ids every
        team they belong to.
      responses:
        '200':
          description: OK
        '401':
          description: Unauthorized
  /api/v1/auth/token:
    post:
      operationId: exchangeToken
      summary: Exchange an Auth0 access token for a session
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                access_token:
                  type: string
      responses:
        '200':
          description: OK
        '401':
          description: Unauthorized
  /loginCB:
    get:
      operationId: loginCallback
      summary: Auth0 login callback; redirects to the frontend
      parameters:
        - in: query
          name: code
          schema:
            type: string
        - in: query
          name: state
          schema:
            type: string
      responses:
        '302':
          description: Redirect to frontend
