{
  "openapi": "3.1.0",
  "info": {
    "title": "Proload API",
    "version": "0.1.0",
    "description": "Lifting math as a service: 1RM estimation, plate loading, training volume, strength scores, and RPE — all from a single deterministic API.",
    "contact": {
      "name": "Proload",
      "url": "https://proload.online",
      "email": "support@proload.online"
    },
    "license": {
      "name": "Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://api.proload.online/v1",
      "description": "Production"
    },
    {
      "url": "http://localhost:7071/v1",
      "description": "Local development"
    }
  ],
  "tags": [
    {
      "name": "Health",
      "description": "Service liveness probes."
    },
    {
      "name": "Calculators",
      "description": "Deterministic lifting calculators."
    }
  ],
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "Ocp-Apim-Subscription-Key",
        "description": "Per-tier API key issued by api.proload.online."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "example": "validation_error"
              },
              "message": {
                "type": "string"
              },
              "details": {},
              "requestId": {
                "type": "string"
              }
            }
          }
        }
      },
      "Unit": {
        "type": "string",
        "enum": [
          "kg",
          "lb"
        ]
      },
      "OneRepMaxFormula": {
        "type": "string",
        "enum": [
          "epley",
          "brzycki",
          "lombardi",
          "wathan",
          "oconner",
          "mayhew"
        ]
      },
      "PlateInventoryItem": {
        "type": "object",
        "required": [
          "weight",
          "count"
        ],
        "properties": {
          "weight": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 1000,
            "example": 45
          },
          "count": {
            "type": "integer",
            "minimum": 0,
            "maximum": 200,
            "example": 16
          }
        }
      },
      "PlateRequest": {
        "type": "object",
        "required": [
          "target"
        ],
        "additionalProperties": false,
        "properties": {
          "target": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 10000,
            "example": 315
          },
          "barWeight": {
            "type": "number",
            "minimum": 0,
            "maximum": 500,
            "default": 45,
            "example": 45
          },
          "collarWeight": {
            "type": "number",
            "minimum": 0,
            "maximum": 50,
            "example": 0
          },
          "plates": {
            "type": "array",
            "minItems": 1,
            "maxItems": 20,
            "items": {
              "$ref": "#/components/schemas/PlateInventoryItem"
            }
          },
          "unit": {
            "$ref": "#/components/schemas/Unit",
            "default": "lb"
          }
        }
      },
      "PlateBreakdownItem": {
        "type": "object",
        "required": [
          "weight",
          "perSide"
        ],
        "properties": {
          "weight": {
            "type": "number"
          },
          "perSide": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "PlateBreakdown": {
        "type": "object",
        "required": [
          "perSide",
          "sidePlateWeight",
          "totalWeight",
          "target",
          "remainder",
          "exact",
          "plateCount"
        ],
        "properties": {
          "perSide": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlateBreakdownItem"
            }
          },
          "sidePlateWeight": {
            "type": "number"
          },
          "totalWeight": {
            "type": "number"
          },
          "target": {
            "type": "number"
          },
          "remainder": {
            "type": "number"
          },
          "exact": {
            "type": "boolean"
          },
          "plateCount": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "PlateResponse": {
        "type": "object",
        "required": [
          "input",
          "greedy",
          "balanced"
        ],
        "properties": {
          "input": {
            "type": "object",
            "required": [
              "target",
              "barWeight",
              "collarWeight",
              "plates",
              "unit"
            ],
            "properties": {
              "target": {
                "type": "number"
              },
              "barWeight": {
                "type": "number"
              },
              "collarWeight": {
                "type": "number"
              },
              "plates": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/PlateInventoryItem"
                }
              },
              "unit": {
                "$ref": "#/components/schemas/Unit"
              }
            }
          },
          "greedy": {
            "$ref": "#/components/schemas/PlateBreakdown"
          },
          "balanced": {
            "$ref": "#/components/schemas/PlateBreakdown"
          }
        }
      },
      "OneRepMaxRequest": {
        "type": "object",
        "required": [
          "weight",
          "reps"
        ],
        "additionalProperties": false,
        "properties": {
          "weight": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 10000,
            "example": 225
          },
          "reps": {
            "type": "integer",
            "minimum": 1,
            "maximum": 36,
            "example": 5
          },
          "unit": {
            "$ref": "#/components/schemas/Unit",
            "default": "lb"
          },
          "formula": {
            "$ref": "#/components/schemas/OneRepMaxFormula"
          }
        }
      },
      "OneRepMaxResultItem": {
        "type": "object",
        "required": [
          "formula",
          "oneRepMax",
          "raw"
        ],
        "properties": {
          "formula": {
            "$ref": "#/components/schemas/OneRepMaxFormula"
          },
          "oneRepMax": {
            "type": "number",
            "example": 262.5
          },
          "raw": {
            "type": "number",
            "example": 262.5
          }
        }
      },
      "OneRepMaxResponse": {
        "type": "object",
        "required": [
          "input",
          "results",
          "average",
          "conservative",
          "aggressive",
          "median",
          "percentTable"
        ],
        "properties": {
          "input": {
            "$ref": "#/components/schemas/OneRepMaxRequest"
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OneRepMaxResultItem"
            },
            "minItems": 6,
            "maxItems": 6
          },
          "average": {
            "type": "number"
          },
          "conservative": {
            "type": "number"
          },
          "aggressive": {
            "type": "number"
          },
          "median": {
            "type": "number"
          },
          "percentTable": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "reps",
                "percent",
                "weight"
              ],
              "properties": {
                "reps": {
                  "type": "integer"
                },
                "percent": {
                  "type": "integer"
                },
                "weight": {
                  "type": "number"
                }
              }
            }
          }
        }
      },
      "HealthResponse": {
        "type": "object",
        "required": [
          "status",
          "service",
          "time"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "service": {
            "type": "string",
            "example": "proload-api"
          },
          "version": {
            "type": "string"
          },
          "time": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "VolumeSet": {
        "type": "object",
        "required": [
          "weight",
          "reps"
        ],
        "additionalProperties": false,
        "properties": {
          "weight": {
            "type": "number",
            "minimum": 0,
            "maximum": 10000,
            "example": 225
          },
          "reps": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "example": 5
          },
          "exercise": {
            "type": "string",
            "maxLength": 80,
            "example": "squat"
          },
          "muscle": {
            "type": "string",
            "maxLength": 40,
            "example": "legs"
          },
          "rpe": {
            "type": "number",
            "minimum": 1,
            "maximum": 10
          }
        }
      },
      "VolumeRequest": {
        "type": "object",
        "required": [
          "sets"
        ],
        "additionalProperties": false,
        "properties": {
          "sets": {
            "type": "array",
            "minItems": 1,
            "maxItems": 200,
            "items": {
              "$ref": "#/components/schemas/VolumeSet"
            }
          },
          "oneRepMaxes": {
            "type": "object",
            "additionalProperties": {
              "type": "number",
              "exclusiveMinimum": 0
            }
          },
          "hardSetMinReps": {
            "type": "integer",
            "minimum": 1,
            "maximum": 20,
            "default": 5
          },
          "hardSetMinIntensity": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "default": 0.65
          },
          "unit": {
            "$ref": "#/components/schemas/Unit",
            "default": "lb"
          }
        }
      },
      "VolumeBreakdownEntry": {
        "type": "object",
        "required": [
          "label",
          "tonnage",
          "sets",
          "reps",
          "hardSets"
        ],
        "properties": {
          "label": {
            "type": "string"
          },
          "tonnage": {
            "type": "number"
          },
          "sets": {
            "type": "integer"
          },
          "reps": {
            "type": "integer"
          },
          "hardSets": {
            "type": "integer"
          }
        }
      },
      "VolumeResponse": {
        "type": "object",
        "required": [
          "input",
          "tonnage",
          "totalSets",
          "hardSets",
          "totalReps",
          "averageReps",
          "averageLoad",
          "byExercise",
          "byMuscle"
        ],
        "properties": {
          "input": {
            "type": "object",
            "properties": {
              "unit": {
                "$ref": "#/components/schemas/Unit"
              }
            }
          },
          "tonnage": {
            "type": "number"
          },
          "totalSets": {
            "type": "integer"
          },
          "hardSets": {
            "type": "integer"
          },
          "totalReps": {
            "type": "integer"
          },
          "averageReps": {
            "type": "number"
          },
          "averageLoad": {
            "type": "number"
          },
          "byExercise": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VolumeBreakdownEntry"
            }
          },
          "byMuscle": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VolumeBreakdownEntry"
            }
          }
        }
      },
      "StrengthFormula": {
        "type": "string",
        "enum": [
          "wilks2020",
          "dots",
          "ipf-gl"
        ]
      },
      "StrengthSex": {
        "type": "string",
        "enum": [
          "male",
          "female"
        ]
      },
      "StrengthScoreRequest": {
        "type": "object",
        "required": [
          "bodyweight",
          "total",
          "sex"
        ],
        "additionalProperties": false,
        "properties": {
          "bodyweight": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 10000,
            "example": 200
          },
          "total": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 10000,
            "example": 1400
          },
          "sex": {
            "$ref": "#/components/schemas/StrengthSex"
          },
          "unit": {
            "$ref": "#/components/schemas/Unit",
            "default": "lb"
          },
          "formulas": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StrengthFormula"
            },
            "minItems": 1,
            "maxItems": 3
          },
          "equipment": {
            "type": "string",
            "enum": [
              "classic",
              "equipped"
            ],
            "default": "classic"
          },
          "event": {
            "type": "string",
            "enum": [
              "sbd",
              "bench"
            ],
            "default": "sbd"
          }
        }
      },
      "StrengthScoreResponse": {
        "type": "object",
        "required": [
          "input",
          "results"
        ],
        "properties": {
          "input": {
            "type": "object",
            "required": [
              "bodyweightKg",
              "totalKg",
              "sex",
              "unit",
              "equipment",
              "event"
            ],
            "properties": {
              "bodyweightKg": {
                "type": "number"
              },
              "totalKg": {
                "type": "number"
              },
              "sex": {
                "$ref": "#/components/schemas/StrengthSex"
              },
              "unit": {
                "$ref": "#/components/schemas/Unit"
              },
              "equipment": {
                "type": "string",
                "enum": [
                  "classic",
                  "equipped"
                ]
              },
              "event": {
                "type": "string",
                "enum": [
                  "sbd",
                  "bench"
                ]
              }
            }
          },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "formula",
                "score",
                "coefficient"
              ],
              "properties": {
                "formula": {
                  "$ref": "#/components/schemas/StrengthFormula"
                },
                "score": {
                  "type": "number"
                },
                "coefficient": {
                  "type": "number"
                }
              }
            }
          }
        }
      },
      "RpeRequest": {
        "type": "object",
        "required": [
          "reps",
          "rpe"
        ],
        "additionalProperties": false,
        "properties": {
          "reps": {
            "type": "integer",
            "minimum": 1,
            "maximum": 36,
            "example": 5
          },
          "rpe": {
            "type": "number",
            "minimum": 1,
            "maximum": 10,
            "example": 8
          },
          "oneRepMax": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 10000
          },
          "weight": {
            "type": "number",
            "exclusiveMinimum": 0,
            "maximum": 10000
          },
          "unit": {
            "$ref": "#/components/schemas/Unit",
            "default": "lb"
          }
        }
      },
      "RpeResponse": {
        "type": "object",
        "required": [
          "input",
          "reps",
          "rpe",
          "percent",
          "extrapolated"
        ],
        "properties": {
          "input": {
            "$ref": "#/components/schemas/RpeRequest"
          },
          "reps": {
            "type": "integer"
          },
          "rpe": {
            "type": "number"
          },
          "percent": {
            "type": "number"
          },
          "extrapolated": {
            "type": "boolean"
          },
          "weight": {
            "type": "number"
          },
          "estimated1RM": {
            "type": "number"
          }
        }
      }
    }
  },
  "security": [
    {
      "apiKey": []
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "tags": [
          "Health"
        ],
        "operationId": "getHealth",
        "summary": "Liveness probe",
        "description": "Unauthenticated liveness probe. Returns service name, version, and current server time. Used by uptime monitors and load balancers.",
        "security": [],
        "responses": {
          "200": {
            "description": "Service is healthy",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "tags": [
          "Health"
        ],
        "operationId": "getOpenApiSpec",
        "summary": "OpenAPI specification",
        "description": "Returns the canonical OpenAPI 3.1 specification for this API. Cache for up to 5 minutes.",
        "security": [],
        "responses": {
          "200": {
            "description": "OpenAPI 3.1 spec",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/one-rep-max": {
      "post": {
        "tags": [
          "Calculators"
        ],
        "operationId": "estimateOneRepMax",
        "summary": "Estimate one-rep max from any rep range",
        "description": "Returns six formula estimates (Epley, Brzycki, Lombardi, Wathan, O'Conner, Mayhew), summary stats (average, median, conservative, aggressive), and a working-weight percent table from 1 to 12 reps.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OneRepMaxRequest"
              },
              "example": {
                "weight": 225,
                "reps": 5,
                "unit": "lb"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "1RM estimate",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OneRepMaxResponse"
                }
              }
            }
          },
          "304": {
            "description": "Not modified (matching ETag)"
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Body too large",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "Unsupported media type",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/plates": {
      "post": {
        "tags": [
          "Calculators"
        ],
        "operationId": "computePlateLoading",
        "summary": "Compute plate loading for a target weight",
        "description": "Returns both greedy (minimum plates) and balanced (visually symmetric) per-side plate breakdowns for a target bar load. Defaults to standard commercial lb or kg plate inventories.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlateRequest"
              },
              "example": {
                "target": 315,
                "barWeight": 45,
                "unit": "lb"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Plate breakdown",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlateResponse"
                }
              }
            }
          },
          "304": {
            "description": "Not modified (matching ETag)"
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/volume": {
      "post": {
        "tags": [
          "Calculators"
        ],
        "operationId": "aggregateVolume",
        "summary": "Aggregate training volume across a list of sets",
        "description": "Returns total tonnage, hard-set count, per-exercise and per-muscle aggregations. Supply `oneRepMaxes` to enable intensity-aware hard-set counting.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VolumeRequest"
              },
              "example": {
                "sets": [
                  {
                    "weight": 225,
                    "reps": 5,
                    "exercise": "squat",
                    "muscle": "legs"
                  },
                  {
                    "weight": 225,
                    "reps": 5,
                    "exercise": "squat",
                    "muscle": "legs"
                  },
                  {
                    "weight": 185,
                    "reps": 8,
                    "exercise": "bench",
                    "muscle": "chest"
                  }
                ],
                "unit": "lb"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Volume report",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VolumeResponse"
                }
              }
            }
          },
          "304": {
            "description": "Not modified (matching ETag)"
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/strength-score": {
      "post": {
        "tags": [
          "Calculators"
        ],
        "operationId": "computeStrengthScore",
        "summary": "Compute Wilks 2020, DOTS, and IPF GL strength scores",
        "description": "Normalises a powerlifting total against bodyweight using up to three coefficient systems. Input may be in lb or kg; the math runs in kg.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StrengthScoreRequest"
              },
              "example": {
                "bodyweight": 200,
                "total": 1400,
                "sex": "male",
                "unit": "lb"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Strength score report",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StrengthScoreResponse"
                }
              }
            }
          },
          "304": {
            "description": "Not modified (matching ETag)"
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/rpe": {
      "post": {
        "tags": [
          "Calculators"
        ],
        "operationId": "convertRpe",
        "summary": "Convert RPE-at-reps to %1RM (and optionally weight or 1RM)",
        "description": "Uses the Tuchscherer / RTS chart. Supply `oneRepMax` to receive a working `weight`, or supply `weight` to receive an `estimated1RM`. Out-of-domain inputs are clamped and flagged via `extrapolated`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RpeRequest"
              },
              "example": {
                "reps": 5,
                "rpe": 8,
                "oneRepMax": 300,
                "unit": "lb"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "RPE conversion",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RpeResponse"
                }
              }
            }
          },
          "304": {
            "description": "Not modified (matching ETag)"
          },
          "400": {
            "description": "Invalid request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  }
}
