openapi: 3.1.0
info:
  title: Paggered Read API
  version: 1.0.0-pilot
  description: >-
    Private server-to-server pilot contract for TRENBot. The bearer token
    authenticates the service client and verified Discord channel headers
    resolve an explicitly connected Paggered account. This is not public
    self-service authentication and it does not support username lookup or
    public writes.
servers:
  - url: /
    description: The current Paggered deployment origin.
security:
  - pilotBearer: []
paths:
  /api/v1/me:
    get:
      operationId: getPilotProfile
      parameters:
        - $ref: "#/components/parameters/Platform"
        - $ref: "#/components/parameters/PlatformUserId"
        - $ref: "#/components/parameters/GuildId"
        - $ref: "#/components/parameters/ChannelId"
      responses:
        "200":
          description: Pilot profile display preferences.
          headers:
            Cache-Control: { $ref: "#/components/headers/CacheControl" }
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProfileResponse"
        default:
          $ref: "#/components/responses/Error"
  /api/v1/stats:
    get:
      operationId: getPilotStats
      parameters:
        - $ref: "#/components/parameters/Platform"
        - $ref: "#/components/parameters/PlatformUserId"
        - $ref: "#/components/parameters/GuildId"
        - $ref: "#/components/parameters/ChannelId"
      responses:
        "200":
          description: Completed-workout counts and consecutive active weeks.
          headers:
            Cache-Control: { $ref: "#/components/headers/CacheControl" }
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StatsResponse"
        default:
          $ref: "#/components/responses/Error"
  /api/v1/workouts:
    get:
      operationId: listPilotWorkouts
      parameters:
        - $ref: "#/components/parameters/Platform"
        - $ref: "#/components/parameters/PlatformUserId"
        - $ref: "#/components/parameters/GuildId"
        - $ref: "#/components/parameters/ChannelId"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Completed workout summaries in stable newest-first order.
          headers:
            Cache-Control: { $ref: "#/components/headers/CacheControl" }
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkoutListResponse"
        default:
          $ref: "#/components/responses/Error"
  /api/v1/workouts/{id}:
    get:
      operationId: getPilotWorkout
      parameters:
        - $ref: "#/components/parameters/Platform"
        - $ref: "#/components/parameters/PlatformUserId"
        - $ref: "#/components/parameters/GuildId"
        - $ref: "#/components/parameters/ChannelId"
        - $ref: "#/components/parameters/Id"
      responses:
        "200":
          description: Structured completed workout performance.
          headers:
            Cache-Control: { $ref: "#/components/headers/CacheControl" }
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkoutResponse"
        default:
          $ref: "#/components/responses/Error"
  /api/v1/saved-workouts:
    get:
      operationId: listPilotSavedWorkouts
      parameters:
        - $ref: "#/components/parameters/Platform"
        - $ref: "#/components/parameters/PlatformUserId"
        - $ref: "#/components/parameters/GuildId"
        - $ref: "#/components/parameters/ChannelId"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Cursor"
      responses:
        "200":
          description: Saved workout titles and exercise counts.
          headers:
            Cache-Control: { $ref: "#/components/headers/CacheControl" }
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SavedWorkoutListResponse"
        default:
          $ref: "#/components/responses/Error"
  /api/v1/saved-workouts/{id}:
    get:
      operationId: getPilotSavedWorkout
      parameters:
        - $ref: "#/components/parameters/Platform"
        - $ref: "#/components/parameters/PlatformUserId"
        - $ref: "#/components/parameters/GuildId"
        - $ref: "#/components/parameters/ChannelId"
        - $ref: "#/components/parameters/Id"
      responses:
        "200":
          description: Ordered saved-workout prescription.
          headers:
            Cache-Control: { $ref: "#/components/headers/CacheControl" }
            X-Request-Id: { $ref: "#/components/headers/RequestId" }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SavedWorkoutResponse"
        default:
          $ref: "#/components/responses/Error"
components:
  headers:
    CacheControl:
      schema: { type: string, const: "private, no-store" }
    RequestId:
      schema: { type: string, format: uuid }
  securitySchemes:
    pilotBearer:
      type: http
      scheme: bearer
      bearerFormat: opaque pilot token
      description: Server-only token authenticating the single trusted TRENBot client.
  parameters:
    Platform:
      name: X-Paggered-Platform
      in: header
      required: true
      schema: { type: string, const: discord }
    PlatformUserId:
      name: X-Paggered-Platform-User-Id
      in: header
      required: true
      description: Opaque Discord user ID of the command caller.
      schema: { type: string, pattern: "^[0-9]{5,32}$" }
    GuildId:
      name: X-Paggered-Guild-Id
      in: header
      required: true
      description: Opaque Discord guild ID verified by TRENBot.
      schema: { type: string, pattern: "^[0-9]{5,32}$" }
    ChannelId:
      name: X-Paggered-Channel-Id
      in: header
      required: true
      description: Opaque Discord channel ID verified by TRENBot.
      schema: { type: string, pattern: "^[0-9]{5,32}$" }
    Id:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 10, default: 5 }
    Cursor:
      name: cursor
      in: query
      schema: { type: string }
  responses:
    Error:
      description: Stable API error. Disabled APIs return 404; invalid tokens 401; unconnected channels 403; limits 429.
      headers:
        Cache-Control: { schema: { type: string, const: "private, no-store" } }
        X-Request-Id: { schema: { type: string, format: uuid } }
        Retry-After: { schema: { type: integer }, description: Present for 429 responses. }
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
  schemas:
    Envelope:
      type: object
      required: [data, request_id]
      properties:
        data: {}
        request_id: { type: string, format: uuid }
    ProfileResponse:
      allOf:
        - $ref: "#/components/schemas/Envelope"
        - type: object
          properties:
            data:
              type: object
              required: [display_name, unit_preference, timezone]
              properties:
                display_name: { type: string }
                unit_preference: { type: string, enum: [kg, lbs] }
                timezone: { type: string }
    StatsResponse:
      allOf:
        - $ref: "#/components/schemas/Envelope"
        - type: object
          properties:
            data:
              type: object
              required:
                [
                  workouts_this_week,
                  workouts_last_30_days,
                  consecutive_active_weeks,
                  streak_is_current,
                  timezone,
                ]
              properties:
                workouts_this_week: { type: integer, minimum: 0 }
                workouts_last_30_days: { type: integer, minimum: 0 }
                consecutive_active_weeks: { type: integer, minimum: 0 }
                streak_is_current: { type: boolean }
                timezone: { type: string }
    WorkoutSummary:
      type: object
      required: [id, title, started_at, ended_at, duration_seconds]
      properties:
        id: { type: string, format: uuid }
        title: { type: string }
        started_at: { type: string, format: date-time }
        ended_at: { type: string, format: date-time }
        duration_seconds: { type: [integer, "null"], minimum: 0 }
    WorkoutSet:
      type: object
      required:
        [set_number, set_type, weight_kg, reps, duration_seconds, distance_metres, incline_percent, records]
      properties:
        set_number: { type: integer, minimum: 1 }
        set_type: { type: string }
        weight_kg: { type: [number, "null"] }
        reps: { type: [number, "null"] }
        duration_seconds: { type: [number, "null"] }
        distance_metres: { type: [number, "null"] }
        incline_percent: { type: [number, "null"] }
        records:
          type: array
          description: >-
            Metrics for which this set beat every eligible set completed earlier for the exercise.
            Later workouts do not erase this historical indicator; equal values are not records.
          uniqueItems: true
          items:
            $ref: "#/components/schemas/StrengthRecordMetric"
    StrengthRecordMetric:
      type: string
      enum: [weight, reps, set_volume, estimated_1rm]
    WorkoutExercise:
      type: object
      required: [name, tracking_type, sets]
      properties:
        name: { type: string }
        tracking_type: { type: string }
        sets: { type: array, items: { $ref: "#/components/schemas/WorkoutSet" } }
    WorkoutDetail:
      allOf:
        - $ref: "#/components/schemas/WorkoutSummary"
        - type: object
          required: [exercises]
          properties:
            exercises: { type: array, items: { $ref: "#/components/schemas/WorkoutExercise" } }
    WorkoutResponse:
      allOf:
        - $ref: "#/components/schemas/Envelope"
        - type: object
          properties: { data: { $ref: "#/components/schemas/WorkoutDetail" } }
    WorkoutListResponse:
      allOf:
        - $ref: "#/components/schemas/Envelope"
        - type: object
          properties:
            data:
              type: object
              required: [items, next_cursor]
              properties:
                items:
                  {
                    type: array,
                    maxItems: 10,
                    items: { $ref: "#/components/schemas/WorkoutSummary" },
                  }
                next_cursor: { type: [string, "null"] }
    SavedWorkoutSummary:
      type: object
      required: [id, title, exercise_count, updated_at]
      properties:
        id: { type: string, format: uuid }
        title: { type: string }
        exercise_count: { type: integer, minimum: 0 }
        updated_at: { type: string, format: date-time }
    SavedWorkoutExercise:
      type: object
      required: [name, muscle_group, sets, reps, rest_seconds]
      properties:
        name: { type: string }
        muscle_group: { type: [string, "null"] }
        sets: { type: number, minimum: 0 }
        reps: { type: string }
        rest_seconds: { type: [number, "null"], minimum: 0 }
    SavedWorkoutDetail:
      type: object
      required: [id, title, updated_at, exercises]
      properties:
        id: { type: string, format: uuid }
        title: { type: string }
        updated_at: { type: string, format: date-time }
        exercises: { type: array, items: { $ref: "#/components/schemas/SavedWorkoutExercise" } }
    SavedWorkoutResponse:
      allOf:
        - $ref: "#/components/schemas/Envelope"
        - type: object
          properties: { data: { $ref: "#/components/schemas/SavedWorkoutDetail" } }
    SavedWorkoutListResponse:
      allOf:
        - $ref: "#/components/schemas/Envelope"
        - type: object
          properties:
            data:
              type: object
              required: [items, next_cursor]
              properties:
                items:
                  {
                    type: array,
                    maxItems: 10,
                    items: { $ref: "#/components/schemas/SavedWorkoutSummary" },
                  }
                next_cursor: { type: [string, "null"] }
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message, request_id]
          properties:
            code:
              {
                type: string,
                enum:
                  [
                    invalid_request,
                    invalid_cursor,
                    invalid_token,
                    invalid_platform_context,
                    connection_required,
                    scope_denied,
                    rate_limited,
                    not_found,
                    server_error,
                  ],
              }
            message: { type: string }
            request_id: { type: string, format: uuid }
