> For the complete documentation index, see [llms.txt](https://docs.perfectscale.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.perfectscale.io/api/public-api.md).

# Public API

The API is exclusively available for our paying customers, allowing them to interact with the platform programmatically. \
Check the following information to get acquainted with the PerfectScale [rate-limiting policy](#rate-limiting) and [authentication process](#authentication).&#x20;

{% hint style="info" %}
The PerfectScale public API is a premium feature available only with our EXPERT package for PerfectScale’s paying customers. Contact <sales@perfectscale.io> to learn more about how to gain access to this feature.
{% endhint %}

PerfectScale API implements a rate-limiting policy to ensure fair usage and maintain the quality of service. The rate limit is set at **10 requests per minute** per client.

## Authentication

To interact with the PerfectScale API, a token is required. To get the token, follow the steps below:

1. **Retrieve Client Credentials**:
   * Go to [https://app.perfectscale.io](https://app.perfectscale.io/).
   * Click on your user avatar located at the bottom left corner of the page.
   * Select **Organization Settings** from the menu.
   * In the pop-up window, navigate to the **API Tokens** tab.
   * Click on **Generate Token**.
   * Assign a **Read Only** Role to the new token.
   * Upon creation, you will be provided with a `client_id` (Client ID) and `client_secret` (Secret Key).
2. **Obtain Access Token**:

   * Make a POST request to the [Authentication Endpoint](#authentication-endpoint) with the following payload:

   ```json
   {
     "client_id": "your_client_id",
     "client_secret": "your_client_secret"
   }
   ```

   * The API will respond with a token payload, which will contain your access token.
3. **Access Other Endpoints**:

   * With the obtained access token, you can make authorized requests to other endpoints of the PerfectScale API. Include the token in the Authorization header of your HTTP requests as follows:

   ```http
   Authorization: Bearer your_access_token
   ```

### Authentication endpoint

{% openapi src="/files/KMk1HD8syQcGTx0QMC8m" path="/auth/public\_auth" method="post" %}
[openapi-final.yaml](https://1573387604-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FABMqnYtsOO44JmQTVSnn%2Fuploads%2FrPCvY2lic8IXN4oAqagO%2Fopenapi-final.yaml?alt=media\&token=3383b5c3-1bec-4b1a-9278-55e652357cb5)
{% endopenapi %}

### Clusters endpoint

## GET /clusters

>

```json
{"openapi":"3.0.3","info":{"title":"Perfectscale API - OpenAPI 3.0","version":"v1"},"servers":[{"url":"https://api.app.perfectscale.io/public/v1"}],"security":[{"auth":[]}],"components":{"securitySchemes":{"auth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"Cluster":{"type":"object","properties":{"uid":{"nullable":true,"type":"string"},"name":{"type":"string"},"cloud":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/ClusterCloud"}]},"region":{"nullable":true,"type":"string"},"createdAt":{"type":"string","format":"date-time"},"lastTransmittedAt":{"type":"string","format":"date-time"}},"required":["uid","name","createdAt","lastTransmittedAt"]},"ClusterCloud":{"type":"string","enum":["aws","gcp","azure"]}}},"paths":{"/clusters":{"get":{"operationId":"GetClusters","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Cluster"}}},"required":["data"]}}}}}}}}}
```

## GET /clusters/{cluster\_uid}

>

```json
{"openapi":"3.0.3","info":{"title":"Perfectscale API - OpenAPI 3.0","version":"v1"},"servers":[{"url":"https://api.app.perfectscale.io/public/v1"}],"security":[{"auth":[]}],"components":{"securitySchemes":{"auth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"ClusterDetail":{"type":"object","properties":{"uid":{"nullable":true,"type":"string"},"name":{"type":"string"},"cloud":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/ClusterCloud"}]},"region":{"nullable":true,"type":"string"},"createdAt":{"type":"string","format":"date-time"},"lastTransmittedAt":{"type":"string","format":"date-time"},"emission":{"type":"object","additionalProperties":{"type":"number","format":"double"}}},"required":["uid","name","createdAt","lastTransmittedAt","emission"]},"ClusterCloud":{"type":"string","enum":["aws","gcp","azure"]}}},"paths":{"/clusters/{cluster_uid}":{"get":{"operationId":"GetCluster","parameters":[{"name":"cluster_uid","in":"path","required":true,"schema":{"type":"string"}},{"name":"period","in":"query","required":false,"schema":{"type":"string"},"description":"The period for which carbon emission is calculated"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ClusterDetail"}},"required":["data"]}}}}}}}}}
```

### Workloads endpoint

{% hint style="warning" %}
Ensure that the following request includes the **`cluster_uid`** parameter, as it is **mandatory**. Follow [the instructions](#how-to-get-cluster_uid) provided to obtain this value.
{% endhint %}

## GET /clusters/{cluster\_uid}/workloads

> List all workloads in a specified cluster

```json
{"openapi":"3.0.3","info":{"title":"Perfectscale API - OpenAPI 3.0","version":"v1"},"servers":[{"url":"https://api.app.perfectscale.io/public/v1"}],"security":[{"auth":[]}],"components":{"securitySchemes":{"auth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"TimeWindow":{"type":"string","description":"The time period for which the data is calculated (from 1h to 30d).","enum":["1h","4h","12h","1d","3d","7d","14d","30d"]},"Workload":{"type":"object","properties":{"id":{"type":"string","description":"A unique identifier for the workload, usually following a namespace-type-name convention."},"name":{"type":"string","description":"A human-readable name for the workload."},"type":{"type":"string","description":"The type of Kubernetes workload. Common types include Deployment, StatefulSet, and DaemonSet, but other string values can also be used to represent custom or extended workload types."},"namespace":{"type":"string","description":"The Kubernetes namespace in which the workload resides."},"firstSeen":{"type":"string","format":"date-time","description":"The timestamp when the workload was first observed."},"lastSeen":{"type":"string","format":"date-time","description":"The timestamp when the workload was last observed."},"runningMinutes":{"type":"integer","description":"The total number of minutes the workload has been running."},"replicasCounts":{"$ref":"#/components/schemas/ReplicasCounts"},"resilienceLevel":{"type":"string","description":"**Deprecated**: Use `optimizationPolicy` instead. The resilience level of the workload (configured on Perfectscale side).\n\nMapping to optimizationPolicy values:\n- `low` → `maxSavings`\n- `medium` → `balanced`\n- `high` → `extraHeadroom`\n- `highest` → `maxHeadroom`\n","deprecated":true,"enum":["low","medium","high","highest"]},"optimizationPolicy":{"allOf":[{"$ref":"#/components/schemas/OptimizationPolicy"}],"description":"The optimization policy applied to the workload."},"optimizationPolicyTimeWindow":{"allOf":[{"$ref":"#/components/schemas/OptimizationPolicyTimeWindow"}],"description":"The time window for optimization policy."},"cpuOptimizationPolicy":{"allOf":[{"$ref":"#/components/schemas/OptimizationPolicy"}],"description":"The CPU-specific optimization policy."},"memoryOptimizationPolicy":{"allOf":[{"$ref":"#/components/schemas/OptimizationPolicy"}],"description":"The memory-specific optimization policy."},"memoryRequestEqualsLimit":{"type":"boolean","description":"Whether memory request equals limit is enabled."},"muteStatus":{"$ref":"#/components/schemas/MuteStatus"},"costAnalysis":{"$ref":"#/components/schemas/CostAnalysis"},"workloadLabels":{"type":"object","description":"Key-value pairs representing kubernets labels associated with the workload.","additionalProperties":{"type":"string","description":"A label value associated with a particular key."}},"indicators":{"type":"array","description":"A list of indicators providing insights into the performance and health of the workload.","items":{"$ref":"#/components/schemas/Indicator"}},"containers":{"type":"array","description":"An object holding detailed information about each container within the workload.","items":{"$ref":"#/components/schemas/Container"}}},"required":["id","name","type","namespace","firstSeen","lastSeen","runningMinutes","replicasCounts","resilienceLevel","optimizationPolicy","optimizationPolicyTimeWindow","cpuOptimizationPolicy","memoryOptimizationPolicy","memoryRequestEqualsLimit","muteStatus","costAnalysis","workloadLabels","indicators","containers"]},"ReplicasCounts":{"type":"object","description":"An object representing the count statistics of replicas for a workload over a month period.","properties":{"maxCount":{"type":"integer","description":"The maximum count of replicas observed for the workload during the month period."},"avgCount":{"type":"integer","description":"The average count of replicas observed for the workload during the month period."}},"required":["maxCount","avgCount"]},"OptimizationPolicy":{"type":"string","description":"The optimization policy determining resource allocation strategy.","enum":["balanced","extraHeadroom","maxHeadroom","maxSavings"]},"OptimizationPolicyTimeWindow":{"type":"string","description":"The time window for optimization policy.","enum":["14d","21d","30d","45d","policyDefault"]},"MuteStatus":{"type":"object","properties":{"isMuted":{"type":"boolean","description":"Indicates whether the workload is currently muted."},"expires":{"type":"string","format":"date-time","nullable":true,"description":"The date and time when the workload will automatically become unmuted. If null, the mute status does not expire automatically."}},"required":["isMuted","expires"]},"CostAnalysis":{"type":"object","description":"Analysis of the costs associated with the workload's resource consumption for the requested period.","properties":{"past30Days":{"allOf":[{"$ref":"#/components/schemas/PastPeriodCostAnalysis"}],"description":"Cost analysis data for a 30-day window when an explicit period is requested (including period=30d). When no period is specified the default 28-day window is used for backward compatibility.\n"},"pastPeriod":{"$ref":"#/components/schemas/PastPeriodCostAnalysis"},"next30Days":{"allOf":[{"$ref":"#/components/schemas/NextPeriodCostAnalysis"}],"description":"Projected cost data for a 30-day window when an explicit period is requested (including period=30d). When no period is specified the default 28-day window is used for backward compatibility.\n"},"nextPeriod":{"$ref":"#/components/schemas/NextPeriodCostAnalysis"}},"required":["past30Days","next30Days","pastPeriod","nextPeriod"]},"PastPeriodCostAnalysis":{"type":"object","description":"Metrics detailing the total and wasted costs associated with the workload's resource consumption over the analysed period.","properties":{"totalCost":{"type":"number","format":"double","description":"The total cost incurred by the workload's resource consumption."},"wastedCost":{"type":"number","format":"double","description":"The cost attributed to over-provisioned or inefficiently utilized resources."},"costPerHour":{"type":"number","format":"double","description":"The cost of the workload per hour."}},"required":["totalCost","wastedCost","costPerHour"]},"NextPeriodCostAnalysis":{"type":"object","description":"Metrics detailing the cost increase and potential savings associated with the workload's upcoming potential resource consumption.","properties":{"costIncrease":{"type":"number","format":"double","description":"The workload total cost increase."},"potentialSavings":{"type":"number","format":"double","description":"The potential savings for the workload."}},"required":["costIncrease","potentialSavings"]},"Indicator":{"type":"object","description":"An object representing key performance indicators that provide insights into the performance and health of the workload or container.","properties":{"name":{"type":"string","description":"The name of the indicator representing a specific aspect of performance or health.","enum":["OOM","CpuThrottling","CpuRequestNotSet","MemRequestNotSet","MemLimitNotSet","UnderProvisionedMemRequest","UnderProvisionedMemLimit","UnderProvisionedCpuRequest","UnderProvisionedCpuLimit","OverProvisionedCpuRequest","OverProvisionedMemRequest","RestartsObserved"]},"type":{"type":"string","description":"The type of indicator, categorizing it as either a risk, waste, or no issue (none).","enum":["none","risk","waste"]},"severityLevel":{"type":"integer","description":"The severity level of the indicator, ranging from 0 (no issue) to 3 (critical issue).","enum":[0,1,2,3]}},"required":["name","type","severityLevel"]},"Container":{"type":"object","description":"Detailed information about a specific container within the workload, including its performance indicators and usage statistics.","properties":{"name":{"type":"string","description":"The name of the container."},"runningMinutes":{"type":"integer","description":"The total number of minutes the container has been running."},"indicators":{"type":"array","description":"A list of indicators providing insights into the performance and health of the container.","items":{"$ref":"#/components/schemas/Indicator"}},"resources":{"type":"object","description":"The resource allocation details for the workload, including the current and recommended by PerfectScale allocation.","properties":{"current":{"$ref":"#/components/schemas/Resources"},"recommended":{"$ref":"#/components/schemas/Resources"}},"required":["current","recommended"]},"usage":{"type":"object","description":"An object representing the resource usage statistics of a container, broken down into CPU and memory consumption.","properties":{"cpuCores":{"$ref":"#/components/schemas/Percentiles"},"memoryMiB":{"$ref":"#/components/schemas/Percentiles"}},"required":["cpuCores","memoryMiB"]}},"required":["name","runningMinutes","indicators","resources","usage"]},"Resources":{"type":"object","description":"Metrics detailing the resource allocation in terms of memory and CPU.","properties":{"memoryRequestMiB":{"type":"number","format":"double","description":"The amount of memory requested for the workload in MiB."},"memoryLimitMiB":{"type":"number","format":"double","description":"The maximum amount of memory that can be allocated for the workload in MiB."},"cpuRequestCores":{"type":"number","format":"double","description":"The amount of CPU resources requested for the workload in cores."},"cpuLimitCores":{"type":"number","format":"double","description":"The maximum amount of CPU resources that can be allocated for the workload in cores."}},"required":["memoryRequestMiB","memoryLimitMiB","cpuRequestCores","cpuLimitCores"]},"Percentiles":{"type":"object","description":"An object representing percentile statistics of resource usage, providing insights into the distribution of usage values over a certain period.","properties":{"p90":{"type":"number","format":"double","description":"The 90th percentile value of resource usage, indicating that 90% of the usage values are below this value and 10% are above."},"p95":{"type":"number","format":"double","description":"The 95th percentile value of resource usage, indicating that 95% of the usage values are below this value and 5% are above."},"p100":{"type":"number","format":"double","description":"The 100th percentile value (or maximum value) of resource usage observed during the specified period."}},"required":["p90","p95","p100"]}}},"paths":{"/clusters/{cluster_uid}/workloads":{"get":{"tags":["Workloads"],"summary":"List all workloads in a specified cluster","parameters":[{"name":"cluster_uid","in":"path","required":true,"schema":{"type":"string"},"description":"The unique identifier for the cluster"},{"name":"period","in":"query","required":false,"schema":{"allOf":[{"$ref":"#/components/schemas/TimeWindow"}]},"description":"The period for which data is calculated"},{"name":"expandEphemeralWorkloads","in":"query","required":false,"schema":{"type":"boolean","default":false},"description":"By default, short-lived ephemeral workloads in large clusters are collapsed into a single aggregate row named \"Collapsed Ephemeral Pods\" (namespace \"EphemeralPods\", type \"-\") carrying their summed cost and no recommendations. The aggregate row has empty-string values for resilienceLevel, optimizationPolicy, cpuOptimizationPolicy, memoryOptimizationPolicy and optimizationPolicyTimeWindow, a single placeholder container named \"-\", and firstSeen/lastSeen spanning the collapsed workloads. Collapse eligibility is evaluated within the requested period's data window, so the same cluster may collapse for one period and not another. Set to true to return the full, uncollapsed workload list.\n"}],"responses":{"200":{"description":"A list of workloads","content":{"application/json":{"schema":{"type":"object","properties":{"meta":{"type":"object","properties":{"count":{"type":"integer","description":"The total number of workloads in the specified cluster."}},"required":["count"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/Workload"}}},"required":["meta","data"]}}}},"404":{"description":"Cluster not found"},"500":{"description":"Internal server error"}}}}}}
```

### Infrafit endpoint

The InfraFit API turns PerfectScale recommendations into automation-ready JSON, making it easy to embed cost and resource optimization directly into engineering workflows.&#x20;

You can seamlessly use it for scheduled optimization checks, pre-deployment CI/CD guardrails, automated PRs, and continuous remediation.&#x20;

For Karpenter, the API can return a ready-to-apply NodePool configuration, enabling teams to move from recommendation to implementation without manually translating dashboard insights into infrastructure code.

## List all node groups with infra-fit data and recommendations

> Returns node groups for a cluster, each containing utilization metrics, cost data, risk indicators, and actionable recommendations (instance-type or Karpenter configuration changes). Tenant resolved from service token.<br>

```json
{"openapi":"3.1.0","info":{"title":"Perfectscale API - OpenAPI 3.1","version":"v1"},"servers":[{"url":"https://api.app.perfectscale.io/public/v1"}],"security":[{"auth":[]}],"components":{"securitySchemes":{"auth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"InfraFitPeriod":{"type":"string","description":"Analysis time period for infra-fit data.","enum":["PT1H","PT4H","PT12H","P1D","P3D","P1W","P2W","P4W","P30D","P45D"],"default":"P30D"},"InfraFitAutoscalerType":{"type":"string","enum":["karpenter","cluster_autoscaler","unknown"],"description":"Autoscaler managing a node group"},"InfraFitNodeGroup":{"type":"object","description":"A node group with utilization metrics, cost data, and recommendations.","properties":{"id":{"type":"string","description":"Node pool identifier used in single-group requests"},"architectures":{"type":"array","items":{"$ref":"#/components/schemas/InfraFitArchitecture"},"description":"CPU architectures present in the group"},"reservations":{"type":"array","items":{"$ref":"#/components/schemas/InfraFitReservation"},"description":"Reservation types present in the group"},"autoscalerType":{"description":"Autoscaler managing this group","allOf":[{"$ref":"#/components/schemas/InfraFitAutoscalerType"}]},"nodes":{"$ref":"#/components/schemas/InfraFitNodes"},"runningMinutes":{"type":"integer","description":"Total running minutes across all nodes in the period"},"pods":{"$ref":"#/components/schemas/InfraFitPods"},"cost":{"$ref":"#/components/schemas/InfraFitCost"},"cpu":{"$ref":"#/components/schemas/InfraFitCpuStats"},"mem":{"$ref":"#/components/schemas/InfraFitMemStats"},"gpu":{"oneOf":[{"$ref":"#/components/schemas/InfraFitGpuStats"},{"type":"null"}]},"seen":{"$ref":"#/components/schemas/InfraFitSeenTimestamps"},"labels":{"type":"object","additionalProperties":{"type":"string"},"description":"Kubernetes labels on the node group"},"nodeTypes":{"type":"array","items":{"$ref":"#/components/schemas/InfraFitNodeType"},"description":"Instance types running in this group"},"recommendations":{"$ref":"#/components/schemas/InfraFitRecommendations"}},"required":["id","architectures","reservations","autoscalerType","nodes","runningMinutes","pods","cost","cpu","mem","gpu","seen","labels","nodeTypes","recommendations"]},"InfraFitArchitecture":{"type":"string","enum":["amd64","arm64"],"description":"CPU architecture of nodes in an InfraFit node group"},"InfraFitReservation":{"type":"string","enum":["on_demand","spot","reserved"],"description":"Reservation type of nodes in an InfraFit node group"},"InfraFitNodes":{"type":"object","description":"Node count statistics for the analysis period","properties":{"min":{"type":"integer","description":"Minimum observed node count in the period"},"max":{"type":"integer","description":"Maximum observed node count in the period"},"avg":{"type":"number","format":"double","description":"Average node count in the period"}},"required":["min","max","avg"]},"InfraFitPods":{"type":"object","description":"Pod count statistics for the node group","properties":{"capacity":{"type":"integer","description":"Maximum number of pods the node group can schedule"},"allocatable":{"type":"integer","description":"Maximum number of pods allocatable on the node group (Kubernetes limit per node × node count)"},"avgCount":{"type":"number","format":"double","description":"Average number of pods running in the group"}},"required":["capacity","allocatable","avgCount"]},"InfraFitCost":{"type":"object","description":"Cost breakdown for the analysis period","properties":{"hourly":{"$ref":"#/components/schemas/Money"},"timeframe":{"$ref":"#/components/schemas/Money"},"idle":{"$ref":"#/components/schemas/InfraFitCostIdle"}},"required":["hourly","timeframe","idle"]},"Money":{"type":"object","description":"Monetary amount with currency","properties":{"amount":{"type":"string","description":"Decimal string representation of the amount (e.g. \"100.50\")"},"currency":{"type":"string","description":"ISO 4217 currency code (e.g. \"USD\")"}},"required":["amount","currency"]},"InfraFitCostIdle":{"type":"object","description":"Idle cost breakdown by resource type","properties":{"total":{"$ref":"#/components/schemas/Money"},"cpu":{"$ref":"#/components/schemas/Money"},"gpu":{"oneOf":[{"$ref":"#/components/schemas/Money"},{"type":"null"}]},"mem":{"$ref":"#/components/schemas/Money"}},"required":["total","cpu","gpu","mem"]},"InfraFitCpuStats":{"type":"object","description":"CPU utilization statistics (cores)","properties":{"requested":{"type":"object","description":"CPU cores requested statistics","properties":{"minCores":{"type":"number","format":"double","description":"Minimum CPU cores requested over the requested period"},"maxCores":{"type":"number","format":"double","description":"Maximum CPU cores requested over the requested period"},"avgCores":{"type":"number","format":"double","description":"Time-weighted median CPU cores requested over the requested period (single aggregate, not per-datapoint)"},"p80Cores":{"type":"number","format":"double","description":"80th percentile CPU cores requested over the requested period"},"p95Cores":{"type":"number","format":"double","description":"95th percentile CPU cores requested over the requested period"},"p99Cores":{"type":"number","format":"double","description":"99th percentile CPU cores requested over the requested period"},"p999Cores":{"type":"number","format":"double","description":"99.9th percentile CPU cores requested over the requested period"}},"required":["avgCores","minCores","maxCores","p80Cores","p95Cores","p99Cores","p999Cores"]},"used":{"type":"object","description":"CPU cores used statistics","properties":{"minCores":{"type":"number","format":"double","description":"Minimum CPU cores used over the requested period"},"maxCores":{"type":"number","format":"double","description":"Maximum CPU cores used over the requested period"},"avgCores":{"type":"number","format":"double","description":"Time-weighted median CPU cores used over the requested period (single aggregate, not per-datapoint)"},"p80Cores":{"type":"number","format":"double","description":"80th percentile CPU cores used over the requested period"},"p95Cores":{"type":"number","format":"double","description":"95th percentile CPU cores used over the requested period"},"p99Cores":{"type":"number","format":"double","description":"99th percentile CPU cores used over the requested period"},"p999Cores":{"type":"number","format":"double","description":"99.9th percentile CPU cores used over the requested period"}},"required":["avgCores","minCores","maxCores","p80Cores","p95Cores","p99Cores","p999Cores"]},"idleCores":{"type":"number","format":"double","description":"Idle (unallocated) CPU cores — max(0, allocatable − avg used)"}},"required":["requested","used","idleCores"]},"InfraFitMemStats":{"type":"object","description":"Memory utilization statistics (MiB)","properties":{"requested":{"type":"object","description":"Memory requested statistics","properties":{"minMiB":{"type":"number","format":"double","description":"Minimum memory requested in MiB over the requested period"},"maxMiB":{"type":"number","format":"double","description":"Maximum memory requested in MiB over the requested period"},"avgMiB":{"type":"number","format":"double","description":"Time-weighted median memory requested in MiB over the requested period (single aggregate, not per-datapoint)"},"p80MiB":{"type":"number","format":"double","description":"80th percentile memory requested in MiB over the requested period"},"p95MiB":{"type":"number","format":"double","description":"95th percentile memory requested in MiB over the requested period"},"p99MiB":{"type":"number","format":"double","description":"99th percentile memory requested in MiB over the requested period"},"p999MiB":{"type":"number","format":"double","description":"99.9th percentile memory requested in MiB over the requested period"}},"required":["avgMiB","minMiB","maxMiB","p80MiB","p95MiB","p99MiB","p999MiB"]},"used":{"type":"object","description":"Memory used statistics","properties":{"minMiB":{"type":"number","format":"double","description":"Minimum memory used in MiB over the requested period"},"maxMiB":{"type":"number","format":"double","description":"Maximum memory used in MiB over the requested period"},"avgMiB":{"type":"number","format":"double","description":"Time-weighted median memory used in MiB over the requested period (single aggregate, not per-datapoint)"},"p80MiB":{"type":"number","format":"double","description":"80th percentile memory used in MiB over the requested period"},"p95MiB":{"type":"number","format":"double","description":"95th percentile memory used in MiB over the requested period"},"p99MiB":{"type":"number","format":"double","description":"99th percentile memory used in MiB over the requested period"},"p999MiB":{"type":"number","format":"double","description":"99.9th percentile memory used in MiB over the requested period"}},"required":["avgMiB","minMiB","maxMiB","p80MiB","p95MiB","p99MiB","p999MiB"]},"idleMiB":{"type":"number","format":"double","description":"Idle (unallocated) memory in MiB — max(0, allocatable − avg used)"}},"required":["requested","used","idleMiB"]},"InfraFitGpuStats":{"type":"object","description":"GPU utilization statistics","properties":{"idle":{"$ref":"#/components/schemas/InfraFitIdleGpu"},"requested":{"$ref":"#/components/schemas/InfraFitGpuSample"},"used":{"$ref":"#/components/schemas/InfraFitGpuSample"},"architectures":{"type":"array","items":{"type":"string"},"description":"GPU architectures present in the group (e.g. ampere, hopper), sorted"},"sharingType":{"type":"array","items":{"type":"string"},"description":"Active GPU sharing mechanisms in lower_snake_case (e.g. \"full\", \"time_slicing\", \"mps\", \"mig\")"}},"required":["idle","architectures","requested","used","sharingType"]},"InfraFitIdleGpu":{"type":"object","description":"Idle (unallocated) GPU resources","properties":{"units":{"type":"number","format":"double","description":"Idle (unallocated) GPU units — max(0, allocatable − avg used)"},"memoryMiB":{"type":"number","format":"double","description":"Idle (unallocated) GPU memory in MiB — max(0, capacity − avg used)"}},"required":["units","memoryMiB"]},"InfraFitGpuSample":{"type":"object","description":"GPU sample statistics (time-weighted medians and percentiles)","properties":{"avgUnits":{"type":"number","format":"double","description":"Time-weighted median GPU units"},"minUnits":{"type":"number","format":"double","description":"Minimum GPU units over the period"},"maxUnits":{"type":"number","format":"double","description":"Maximum GPU units over the period"},"p80Units":{"type":"number","format":"double","description":"80th percentile GPU units over the period"},"p95Units":{"type":"number","format":"double","description":"95th percentile GPU units over the period"},"p99Units":{"type":"number","format":"double","description":"99th percentile GPU units over the period"},"p999Units":{"type":"number","format":"double","description":"99.9th percentile GPU units over the period"},"avgMemoryMiB":{"type":"number","format":"double","description":"Time-weighted median GPU memory in MiB"},"minMemoryMiB":{"type":"number","format":"double","description":"Minimum GPU memory in MiB over the period"},"maxMemoryMiB":{"type":"number","format":"double","description":"Maximum GPU memory in MiB over the period"},"p80MemoryMiB":{"type":"number","format":"double","description":"80th percentile GPU memory in MiB over the period"},"p95MemoryMiB":{"type":"number","format":"double","description":"95th percentile GPU memory in MiB over the period"},"p99MemoryMiB":{"type":"number","format":"double","description":"99th percentile GPU memory in MiB over the period"},"p999MemoryMiB":{"type":"number","format":"double","description":"99.9th percentile GPU memory in MiB over the period"}},"required":["avgUnits","minUnits","maxUnits","p80Units","p95Units","p99Units","p999Units","avgMemoryMiB","minMemoryMiB","maxMemoryMiB","p80MemoryMiB","p95MemoryMiB","p99MemoryMiB","p999MemoryMiB"]},"InfraFitSeenTimestamps":{"type":"object","description":"First and last observed timestamps","properties":{"firstTime":{"type":"string","format":"date-time","description":"When first observed (RFC 3339 UTC)"},"lastTime":{"type":"string","format":"date-time","description":"When last observed (RFC 3339 UTC)"}},"required":["firstTime","lastTime"]},"InfraFitNodeType":{"type":"object","description":"Instance type breakdown within a node group","properties":{"id":{"type":"string","description":"Instance type identifier (matches instance.type)"},"instance":{"$ref":"#/components/schemas/InfraFitInstanceInfo"},"isSpot":{"type":"boolean","description":"Whether this instance type runs on spot/preemptible capacity"},"nodes":{"$ref":"#/components/schemas/InfraFitNodes"},"cost":{"$ref":"#/components/schemas/InfraFitCost"},"runningMinutes":{"type":"integer","description":"Total running minutes across all nodes of this instance type in the period"},"pods":{"$ref":"#/components/schemas/InfraFitPods"},"cpu":{"$ref":"#/components/schemas/InfraFitCpuStats"},"mem":{"$ref":"#/components/schemas/InfraFitMemStats"},"gpu":{"oneOf":[{"$ref":"#/components/schemas/InfraFitNodeTypeGpu"},{"type":"null"}]},"seen":{"$ref":"#/components/schemas/InfraFitSeenTimestamps"}},"required":["id","instance","isSpot","nodes","cost","runningMinutes","pods","cpu","mem","gpu","seen"]},"InfraFitInstanceInfo":{"type":"object","description":"Instance type identity and capacity","properties":{"type":{"type":"string","description":"Cloud instance type identifier (e.g. m5.2xlarge)"},"family":{"type":"string","description":"Instance family prefix shared by related instance types (e.g. m5)"},"architecture":{"oneOf":[{"$ref":"#/components/schemas/InfraFitArchitecture"},{"type":"null"}],"description":"CPU architecture of this instance type (null when unknown)"},"cpu":{"$ref":"#/components/schemas/InfraFitInstanceCpu"},"mem":{"$ref":"#/components/schemas/InfraFitInstanceMem"},"gpu":{"oneOf":[{"$ref":"#/components/schemas/InfraFitInstanceGpu"},{"type":"null"}],"description":"GPU capacity, null for non-GPU instance types"}},"required":["type","family","architecture","cpu","mem","gpu"]},"InfraFitInstanceCpu":{"type":"object","description":"CPU capacity for an instance type","properties":{"capacityCores":{"type":"number","format":"double","description":"vCPU count per instance"},"allocatableCores":{"type":"number","format":"double","description":"Allocatable vCPU per instance after Kubernetes system-reserved resources are subtracted"}},"required":["capacityCores","allocatableCores"]},"InfraFitInstanceMem":{"type":"object","description":"Memory capacity for an instance type","properties":{"capacityMiB":{"type":"number","format":"double","description":"Memory capacity per instance in MiB"},"allocatableMiB":{"type":"number","format":"double","description":"Allocatable memory per instance in MiB after Kubernetes system-reserved resources are subtracted"}},"required":["capacityMiB","allocatableMiB"]},"InfraFitInstanceGpu":{"type":"object","description":"GPU capacity for an instance type","properties":{"capacityUnits":{"type":"number","format":"double","description":"GPU unit count per instance"},"allocatableUnits":{"type":"number","format":"double","description":"Allocatable GPU units per instance after Kubernetes system-reserved resources are subtracted"},"memCapacityMiB":{"type":"number","format":"double","description":"Total GPU memory capacity per instance in MiB"}},"required":["capacityUnits","allocatableUnits","memCapacityMiB"]},"InfraFitNodeTypeGpu":{"type":"object","description":"GPU stats for an instance type","properties":{"idle":{"$ref":"#/components/schemas/InfraFitIdleGpu"},"requested":{"$ref":"#/components/schemas/InfraFitGpuSample"},"used":{"$ref":"#/components/schemas/InfraFitGpuSample"},"architectures":{"type":"array","items":{"type":"string"},"description":"GPU architectures present in the instance type (e.g. ampere, hopper), sorted"}},"required":["idle","requested","used","architectures"]},"InfraFitRecommendations":{"description":"Discriminated recommendations object. Routed by autoscalerType: cluster_autoscaler/managed → standard, karpenter → karpenter. Always present, never omitted.\n","oneOf":[{"$ref":"#/components/schemas/InfraFitStandardRecommendations"},{"$ref":"#/components/schemas/InfraFitKarpenterRecommendations"}],"discriminator":{"propertyName":"type","mapping":{"standard":"#/components/schemas/InfraFitStandardRecommendations","karpenter":"#/components/schemas/InfraFitKarpenterRecommendations"}}},"InfraFitStandardRecommendations":{"type":"object","description":"Recommendations for Cluster-Autoscaler and managed node groups","properties":{"type":{"type":"string","enum":["standard"]},"hasChanges":{"type":"boolean","description":"Whether any instance type changes are recommended"},"nodeTypes":{"type":"array","items":{"$ref":"#/components/schemas/InfraFitRecommendedNodeType"},"description":"Recommended instance types, capped at recommendationLimit"}},"required":["type","hasChanges","nodeTypes"]},"InfraFitRecommendedNodeType":{"type":"object","description":"A recommended instance type replacement","properties":{"id":{"type":"string","description":"Unique identifier for this recommendation entry (matches instanceType)"},"instanceType":{"type":"string","description":"Recommended cloud instance type identifier (e.g. m6g.xlarge)"},"instanceFamily":{"type":"string","description":"Instance family prefix of the recommended type (e.g. m6g)"},"hourlyCost":{"$ref":"#/components/schemas/Money"},"estimatedSavings":{"$ref":"#/components/schemas/Money"},"estimatedSavingsPct":{"type":"number","format":"double","description":"Savings as percentage of current group cost"},"nodeCount":{"type":"integer","description":"Recommended number of nodes of this type"}},"required":["id","instanceType","instanceFamily","hourlyCost","estimatedSavings","estimatedSavingsPct","nodeCount"]},"InfraFitKarpenterRecommendations":{"type":"object","description":"Recommendations for Karpenter-managed node pools","properties":{"type":{"type":"string","enum":["karpenter"]},"hasChanges":{"type":"boolean","description":"Whether any configuration changes are recommended"},"currentConfig":{"type":"object","description":"Current raw Karpenter NodePool CR (full JSON)"},"recommendedConfig":{"type":"object","description":"Patched NodePool CR with all recommended changes applied"},"changes":{"type":"array","items":{"$ref":"#/components/schemas/InfraFitKarpenterChange"},"description":"Structured change set (always full, not capped by recommendationLimit)"}},"required":["type","hasChanges","currentConfig","recommendedConfig","changes"]},"InfraFitKarpenterChange":{"type":"object","description":"A single Karpenter configuration change recommendation","properties":{"id":{"type":"string","description":"Unique identifier for this change entry (matches path)"},"title":{"type":"string","description":"Human-readable change title"},"path":{"type":"string","description":"JSONPath (RFC 9535 syntax) into the NodePool CR. Nested keys and array equality-filter selectors are supported, e.g. \".spec.disruption.consolidationPolicy\" or \".spec.template.spec.requirements[?(@.key==\\\"node.kubernetes.io/instance-type\\\")]\".\n"},"operation":{"type":"string","description":"Change operation: \"replace\", \"merge\", or \"remove\". An empty operation is treated as \"replace\"; a replace with a null recommendedValue removes the field.\n"},"currentValue":{"description":"Current value at the path (string, number, boolean, array, object, or null)"},"recommendedValue":{"description":"Recommended value (string, number, boolean, array, object, or null)"},"rationale":{"type":"string","description":"Human-readable explanation for this change"}},"required":["id","title","path","operation","currentValue","recommendedValue","rationale"]},"InfraFitMeta":{"type":"object","description":"Response metadata for InfraFit endpoints","properties":{"timeframe":{"type":"string","description":"Analysis period echoed from the period query parameter (e.g. \"P30D\")"},"pagination":{"description":"Cursor-based pagination metadata. Present in list responses; omitted for single-item responses.","allOf":[{"$ref":"#/components/schemas/PaginationMeta"}]}},"required":["timeframe"]},"PaginationMeta":{"type":"object","description":"Cursor-based pagination metadata for list responses.","properties":{"next":{"type":["string","null"],"description":"Opaque cursor token for the next page, or null if this is the last page"},"prev":{"type":["string","null"],"description":"Opaque cursor token for the previous page, or null if this is the first page"},"pageSize":{"type":"integer","description":"Number of items per page used for this response"}},"required":["next","prev","pageSize"]},"Problem":{"type":"object","description":"RFC 9457 Problem Details error body, served as `application/problem+json`. `code` is a stable machine-readable identifier (e.g. `filter_key_not_supported`, `not_found`, `internal_error`); `retryable` indicates whether retrying the same request may succeed without client-side changes.\n","properties":{"type":{"type":"string","description":"URI reference identifying the problem type."},"title":{"type":"string","description":"Short, human-readable summary of the problem type."},"status":{"type":"integer","description":"HTTP status code generated by the origin server for this occurrence of the problem."},"detail":{"type":"string","description":"Human-readable explanation specific to this occurrence of the problem."},"instance":{"type":"string","description":"URI reference identifying the specific occurrence of the problem."},"code":{"type":"string","description":"Stable machine-readable error code."},"retryable":{"type":"boolean","description":"Whether retrying the same request may succeed without client-side changes."}},"required":["type","title","status","code","retryable"]}}},"paths":{"/clusters/{clusterUid}/node-groups":{"get":{"operationId":"ListInfraFit","tags":["InfraFit"],"summary":"List all node groups with infra-fit data and recommendations","description":"Returns node groups for a cluster, each containing utilization metrics, cost data, risk indicators, and actionable recommendations (instance-type or Karpenter configuration changes). Tenant resolved from service token.\n","parameters":[{"name":"clusterUid","in":"path","required":true,"schema":{"type":"string"},"description":"Cluster unique identifier"},{"name":"period","in":"query","schema":{"allOf":[{"$ref":"#/components/schemas/InfraFitPeriod"}],"default":"P30D"},"description":"Window for the node group's own utilization/idle-cost metrics (CPU, memory, running minutes, unrecognized-group detection). Does not affect recommendation savings/pricing math, which always uses a fixed 30-day window regardless of this value. Default P30D.\n"},{"name":"recommendationLimit","in":"query","schema":{"type":"integer","default":3,"minimum":1,"maximum":20},"description":"Max recommended node types per regular group (Karpenter always returns full change set)"},{"name":"hasRecommendations","in":"query","schema":{"type":"boolean"},"description":"Filter: true=only groups with actionable recs, false=only without, omit=all"},{"name":"includeMuted","in":"query","schema":{"type":"boolean","default":false},"description":"Include muted recommendations"},{"name":"autoscalerType","in":"query","schema":{"$ref":"#/components/schemas/InfraFitAutoscalerType"},"description":"Filter by autoscaler type (e.g. cluster_autoscaler, karpenter)"},{"name":"pageSize","in":"query","schema":{"type":"integer","default":50,"minimum":1,"maximum":500},"description":"Number of items per page (default 50, max 500)"},{"name":"pageToken","in":"query","schema":{"type":"string"},"description":"Opaque cursor token for pagination"}],"responses":{"200":{"description":"List of node groups","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/InfraFitNodeGroup"}},"meta":{"$ref":"#/components/schemas/InfraFitMeta"}},"required":["data","meta"]}}}},"400":{"description":"Invalid parameters","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal server error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}}}
```

## Get a single node group with infra-fit data and recommendations

> Returns a single node group by name. Same shape as one item from the list endpoint. Returns 404 if the group does not exist in the cluster.<br>

```json
{"openapi":"3.1.0","info":{"title":"Perfectscale API - OpenAPI 3.1","version":"v1"},"servers":[{"url":"https://api.app.perfectscale.io/public/v1"}],"security":[{"auth":[]}],"components":{"securitySchemes":{"auth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"InfraFitPeriod":{"type":"string","description":"Analysis time period for infra-fit data.","enum":["PT1H","PT4H","PT12H","P1D","P3D","P1W","P2W","P4W","P30D","P45D"],"default":"P30D"},"InfraFitNodeGroup":{"type":"object","description":"A node group with utilization metrics, cost data, and recommendations.","properties":{"id":{"type":"string","description":"Node pool identifier used in single-group requests"},"architectures":{"type":"array","items":{"$ref":"#/components/schemas/InfraFitArchitecture"},"description":"CPU architectures present in the group"},"reservations":{"type":"array","items":{"$ref":"#/components/schemas/InfraFitReservation"},"description":"Reservation types present in the group"},"autoscalerType":{"description":"Autoscaler managing this group","allOf":[{"$ref":"#/components/schemas/InfraFitAutoscalerType"}]},"nodes":{"$ref":"#/components/schemas/InfraFitNodes"},"runningMinutes":{"type":"integer","description":"Total running minutes across all nodes in the period"},"pods":{"$ref":"#/components/schemas/InfraFitPods"},"cost":{"$ref":"#/components/schemas/InfraFitCost"},"cpu":{"$ref":"#/components/schemas/InfraFitCpuStats"},"mem":{"$ref":"#/components/schemas/InfraFitMemStats"},"gpu":{"oneOf":[{"$ref":"#/components/schemas/InfraFitGpuStats"},{"type":"null"}]},"seen":{"$ref":"#/components/schemas/InfraFitSeenTimestamps"},"labels":{"type":"object","additionalProperties":{"type":"string"},"description":"Kubernetes labels on the node group"},"nodeTypes":{"type":"array","items":{"$ref":"#/components/schemas/InfraFitNodeType"},"description":"Instance types running in this group"},"recommendations":{"$ref":"#/components/schemas/InfraFitRecommendations"}},"required":["id","architectures","reservations","autoscalerType","nodes","runningMinutes","pods","cost","cpu","mem","gpu","seen","labels","nodeTypes","recommendations"]},"InfraFitArchitecture":{"type":"string","enum":["amd64","arm64"],"description":"CPU architecture of nodes in an InfraFit node group"},"InfraFitReservation":{"type":"string","enum":["on_demand","spot","reserved"],"description":"Reservation type of nodes in an InfraFit node group"},"InfraFitAutoscalerType":{"type":"string","enum":["karpenter","cluster_autoscaler","unknown"],"description":"Autoscaler managing a node group"},"InfraFitNodes":{"type":"object","description":"Node count statistics for the analysis period","properties":{"min":{"type":"integer","description":"Minimum observed node count in the period"},"max":{"type":"integer","description":"Maximum observed node count in the period"},"avg":{"type":"number","format":"double","description":"Average node count in the period"}},"required":["min","max","avg"]},"InfraFitPods":{"type":"object","description":"Pod count statistics for the node group","properties":{"capacity":{"type":"integer","description":"Maximum number of pods the node group can schedule"},"allocatable":{"type":"integer","description":"Maximum number of pods allocatable on the node group (Kubernetes limit per node × node count)"},"avgCount":{"type":"number","format":"double","description":"Average number of pods running in the group"}},"required":["capacity","allocatable","avgCount"]},"InfraFitCost":{"type":"object","description":"Cost breakdown for the analysis period","properties":{"hourly":{"$ref":"#/components/schemas/Money"},"timeframe":{"$ref":"#/components/schemas/Money"},"idle":{"$ref":"#/components/schemas/InfraFitCostIdle"}},"required":["hourly","timeframe","idle"]},"Money":{"type":"object","description":"Monetary amount with currency","properties":{"amount":{"type":"string","description":"Decimal string representation of the amount (e.g. \"100.50\")"},"currency":{"type":"string","description":"ISO 4217 currency code (e.g. \"USD\")"}},"required":["amount","currency"]},"InfraFitCostIdle":{"type":"object","description":"Idle cost breakdown by resource type","properties":{"total":{"$ref":"#/components/schemas/Money"},"cpu":{"$ref":"#/components/schemas/Money"},"gpu":{"oneOf":[{"$ref":"#/components/schemas/Money"},{"type":"null"}]},"mem":{"$ref":"#/components/schemas/Money"}},"required":["total","cpu","gpu","mem"]},"InfraFitCpuStats":{"type":"object","description":"CPU utilization statistics (cores)","properties":{"requested":{"type":"object","description":"CPU cores requested statistics","properties":{"minCores":{"type":"number","format":"double","description":"Minimum CPU cores requested over the requested period"},"maxCores":{"type":"number","format":"double","description":"Maximum CPU cores requested over the requested period"},"avgCores":{"type":"number","format":"double","description":"Time-weighted median CPU cores requested over the requested period (single aggregate, not per-datapoint)"},"p80Cores":{"type":"number","format":"double","description":"80th percentile CPU cores requested over the requested period"},"p95Cores":{"type":"number","format":"double","description":"95th percentile CPU cores requested over the requested period"},"p99Cores":{"type":"number","format":"double","description":"99th percentile CPU cores requested over the requested period"},"p999Cores":{"type":"number","format":"double","description":"99.9th percentile CPU cores requested over the requested period"}},"required":["avgCores","minCores","maxCores","p80Cores","p95Cores","p99Cores","p999Cores"]},"used":{"type":"object","description":"CPU cores used statistics","properties":{"minCores":{"type":"number","format":"double","description":"Minimum CPU cores used over the requested period"},"maxCores":{"type":"number","format":"double","description":"Maximum CPU cores used over the requested period"},"avgCores":{"type":"number","format":"double","description":"Time-weighted median CPU cores used over the requested period (single aggregate, not per-datapoint)"},"p80Cores":{"type":"number","format":"double","description":"80th percentile CPU cores used over the requested period"},"p95Cores":{"type":"number","format":"double","description":"95th percentile CPU cores used over the requested period"},"p99Cores":{"type":"number","format":"double","description":"99th percentile CPU cores used over the requested period"},"p999Cores":{"type":"number","format":"double","description":"99.9th percentile CPU cores used over the requested period"}},"required":["avgCores","minCores","maxCores","p80Cores","p95Cores","p99Cores","p999Cores"]},"idleCores":{"type":"number","format":"double","description":"Idle (unallocated) CPU cores — max(0, allocatable − avg used)"}},"required":["requested","used","idleCores"]},"InfraFitMemStats":{"type":"object","description":"Memory utilization statistics (MiB)","properties":{"requested":{"type":"object","description":"Memory requested statistics","properties":{"minMiB":{"type":"number","format":"double","description":"Minimum memory requested in MiB over the requested period"},"maxMiB":{"type":"number","format":"double","description":"Maximum memory requested in MiB over the requested period"},"avgMiB":{"type":"number","format":"double","description":"Time-weighted median memory requested in MiB over the requested period (single aggregate, not per-datapoint)"},"p80MiB":{"type":"number","format":"double","description":"80th percentile memory requested in MiB over the requested period"},"p95MiB":{"type":"number","format":"double","description":"95th percentile memory requested in MiB over the requested period"},"p99MiB":{"type":"number","format":"double","description":"99th percentile memory requested in MiB over the requested period"},"p999MiB":{"type":"number","format":"double","description":"99.9th percentile memory requested in MiB over the requested period"}},"required":["avgMiB","minMiB","maxMiB","p80MiB","p95MiB","p99MiB","p999MiB"]},"used":{"type":"object","description":"Memory used statistics","properties":{"minMiB":{"type":"number","format":"double","description":"Minimum memory used in MiB over the requested period"},"maxMiB":{"type":"number","format":"double","description":"Maximum memory used in MiB over the requested period"},"avgMiB":{"type":"number","format":"double","description":"Time-weighted median memory used in MiB over the requested period (single aggregate, not per-datapoint)"},"p80MiB":{"type":"number","format":"double","description":"80th percentile memory used in MiB over the requested period"},"p95MiB":{"type":"number","format":"double","description":"95th percentile memory used in MiB over the requested period"},"p99MiB":{"type":"number","format":"double","description":"99th percentile memory used in MiB over the requested period"},"p999MiB":{"type":"number","format":"double","description":"99.9th percentile memory used in MiB over the requested period"}},"required":["avgMiB","minMiB","maxMiB","p80MiB","p95MiB","p99MiB","p999MiB"]},"idleMiB":{"type":"number","format":"double","description":"Idle (unallocated) memory in MiB — max(0, allocatable − avg used)"}},"required":["requested","used","idleMiB"]},"InfraFitGpuStats":{"type":"object","description":"GPU utilization statistics","properties":{"idle":{"$ref":"#/components/schemas/InfraFitIdleGpu"},"requested":{"$ref":"#/components/schemas/InfraFitGpuSample"},"used":{"$ref":"#/components/schemas/InfraFitGpuSample"},"architectures":{"type":"array","items":{"type":"string"},"description":"GPU architectures present in the group (e.g. ampere, hopper), sorted"},"sharingType":{"type":"array","items":{"type":"string"},"description":"Active GPU sharing mechanisms in lower_snake_case (e.g. \"full\", \"time_slicing\", \"mps\", \"mig\")"}},"required":["idle","architectures","requested","used","sharingType"]},"InfraFitIdleGpu":{"type":"object","description":"Idle (unallocated) GPU resources","properties":{"units":{"type":"number","format":"double","description":"Idle (unallocated) GPU units — max(0, allocatable − avg used)"},"memoryMiB":{"type":"number","format":"double","description":"Idle (unallocated) GPU memory in MiB — max(0, capacity − avg used)"}},"required":["units","memoryMiB"]},"InfraFitGpuSample":{"type":"object","description":"GPU sample statistics (time-weighted medians and percentiles)","properties":{"avgUnits":{"type":"number","format":"double","description":"Time-weighted median GPU units"},"minUnits":{"type":"number","format":"double","description":"Minimum GPU units over the period"},"maxUnits":{"type":"number","format":"double","description":"Maximum GPU units over the period"},"p80Units":{"type":"number","format":"double","description":"80th percentile GPU units over the period"},"p95Units":{"type":"number","format":"double","description":"95th percentile GPU units over the period"},"p99Units":{"type":"number","format":"double","description":"99th percentile GPU units over the period"},"p999Units":{"type":"number","format":"double","description":"99.9th percentile GPU units over the period"},"avgMemoryMiB":{"type":"number","format":"double","description":"Time-weighted median GPU memory in MiB"},"minMemoryMiB":{"type":"number","format":"double","description":"Minimum GPU memory in MiB over the period"},"maxMemoryMiB":{"type":"number","format":"double","description":"Maximum GPU memory in MiB over the period"},"p80MemoryMiB":{"type":"number","format":"double","description":"80th percentile GPU memory in MiB over the period"},"p95MemoryMiB":{"type":"number","format":"double","description":"95th percentile GPU memory in MiB over the period"},"p99MemoryMiB":{"type":"number","format":"double","description":"99th percentile GPU memory in MiB over the period"},"p999MemoryMiB":{"type":"number","format":"double","description":"99.9th percentile GPU memory in MiB over the period"}},"required":["avgUnits","minUnits","maxUnits","p80Units","p95Units","p99Units","p999Units","avgMemoryMiB","minMemoryMiB","maxMemoryMiB","p80MemoryMiB","p95MemoryMiB","p99MemoryMiB","p999MemoryMiB"]},"InfraFitSeenTimestamps":{"type":"object","description":"First and last observed timestamps","properties":{"firstTime":{"type":"string","format":"date-time","description":"When first observed (RFC 3339 UTC)"},"lastTime":{"type":"string","format":"date-time","description":"When last observed (RFC 3339 UTC)"}},"required":["firstTime","lastTime"]},"InfraFitNodeType":{"type":"object","description":"Instance type breakdown within a node group","properties":{"id":{"type":"string","description":"Instance type identifier (matches instance.type)"},"instance":{"$ref":"#/components/schemas/InfraFitInstanceInfo"},"isSpot":{"type":"boolean","description":"Whether this instance type runs on spot/preemptible capacity"},"nodes":{"$ref":"#/components/schemas/InfraFitNodes"},"cost":{"$ref":"#/components/schemas/InfraFitCost"},"runningMinutes":{"type":"integer","description":"Total running minutes across all nodes of this instance type in the period"},"pods":{"$ref":"#/components/schemas/InfraFitPods"},"cpu":{"$ref":"#/components/schemas/InfraFitCpuStats"},"mem":{"$ref":"#/components/schemas/InfraFitMemStats"},"gpu":{"oneOf":[{"$ref":"#/components/schemas/InfraFitNodeTypeGpu"},{"type":"null"}]},"seen":{"$ref":"#/components/schemas/InfraFitSeenTimestamps"}},"required":["id","instance","isSpot","nodes","cost","runningMinutes","pods","cpu","mem","gpu","seen"]},"InfraFitInstanceInfo":{"type":"object","description":"Instance type identity and capacity","properties":{"type":{"type":"string","description":"Cloud instance type identifier (e.g. m5.2xlarge)"},"family":{"type":"string","description":"Instance family prefix shared by related instance types (e.g. m5)"},"architecture":{"oneOf":[{"$ref":"#/components/schemas/InfraFitArchitecture"},{"type":"null"}],"description":"CPU architecture of this instance type (null when unknown)"},"cpu":{"$ref":"#/components/schemas/InfraFitInstanceCpu"},"mem":{"$ref":"#/components/schemas/InfraFitInstanceMem"},"gpu":{"oneOf":[{"$ref":"#/components/schemas/InfraFitInstanceGpu"},{"type":"null"}],"description":"GPU capacity, null for non-GPU instance types"}},"required":["type","family","architecture","cpu","mem","gpu"]},"InfraFitInstanceCpu":{"type":"object","description":"CPU capacity for an instance type","properties":{"capacityCores":{"type":"number","format":"double","description":"vCPU count per instance"},"allocatableCores":{"type":"number","format":"double","description":"Allocatable vCPU per instance after Kubernetes system-reserved resources are subtracted"}},"required":["capacityCores","allocatableCores"]},"InfraFitInstanceMem":{"type":"object","description":"Memory capacity for an instance type","properties":{"capacityMiB":{"type":"number","format":"double","description":"Memory capacity per instance in MiB"},"allocatableMiB":{"type":"number","format":"double","description":"Allocatable memory per instance in MiB after Kubernetes system-reserved resources are subtracted"}},"required":["capacityMiB","allocatableMiB"]},"InfraFitInstanceGpu":{"type":"object","description":"GPU capacity for an instance type","properties":{"capacityUnits":{"type":"number","format":"double","description":"GPU unit count per instance"},"allocatableUnits":{"type":"number","format":"double","description":"Allocatable GPU units per instance after Kubernetes system-reserved resources are subtracted"},"memCapacityMiB":{"type":"number","format":"double","description":"Total GPU memory capacity per instance in MiB"}},"required":["capacityUnits","allocatableUnits","memCapacityMiB"]},"InfraFitNodeTypeGpu":{"type":"object","description":"GPU stats for an instance type","properties":{"idle":{"$ref":"#/components/schemas/InfraFitIdleGpu"},"requested":{"$ref":"#/components/schemas/InfraFitGpuSample"},"used":{"$ref":"#/components/schemas/InfraFitGpuSample"},"architectures":{"type":"array","items":{"type":"string"},"description":"GPU architectures present in the instance type (e.g. ampere, hopper), sorted"}},"required":["idle","requested","used","architectures"]},"InfraFitRecommendations":{"description":"Discriminated recommendations object. Routed by autoscalerType: cluster_autoscaler/managed → standard, karpenter → karpenter. Always present, never omitted.\n","oneOf":[{"$ref":"#/components/schemas/InfraFitStandardRecommendations"},{"$ref":"#/components/schemas/InfraFitKarpenterRecommendations"}],"discriminator":{"propertyName":"type","mapping":{"standard":"#/components/schemas/InfraFitStandardRecommendations","karpenter":"#/components/schemas/InfraFitKarpenterRecommendations"}}},"InfraFitStandardRecommendations":{"type":"object","description":"Recommendations for Cluster-Autoscaler and managed node groups","properties":{"type":{"type":"string","enum":["standard"]},"hasChanges":{"type":"boolean","description":"Whether any instance type changes are recommended"},"nodeTypes":{"type":"array","items":{"$ref":"#/components/schemas/InfraFitRecommendedNodeType"},"description":"Recommended instance types, capped at recommendationLimit"}},"required":["type","hasChanges","nodeTypes"]},"InfraFitRecommendedNodeType":{"type":"object","description":"A recommended instance type replacement","properties":{"id":{"type":"string","description":"Unique identifier for this recommendation entry (matches instanceType)"},"instanceType":{"type":"string","description":"Recommended cloud instance type identifier (e.g. m6g.xlarge)"},"instanceFamily":{"type":"string","description":"Instance family prefix of the recommended type (e.g. m6g)"},"hourlyCost":{"$ref":"#/components/schemas/Money"},"estimatedSavings":{"$ref":"#/components/schemas/Money"},"estimatedSavingsPct":{"type":"number","format":"double","description":"Savings as percentage of current group cost"},"nodeCount":{"type":"integer","description":"Recommended number of nodes of this type"}},"required":["id","instanceType","instanceFamily","hourlyCost","estimatedSavings","estimatedSavingsPct","nodeCount"]},"InfraFitKarpenterRecommendations":{"type":"object","description":"Recommendations for Karpenter-managed node pools","properties":{"type":{"type":"string","enum":["karpenter"]},"hasChanges":{"type":"boolean","description":"Whether any configuration changes are recommended"},"currentConfig":{"type":"object","description":"Current raw Karpenter NodePool CR (full JSON)"},"recommendedConfig":{"type":"object","description":"Patched NodePool CR with all recommended changes applied"},"changes":{"type":"array","items":{"$ref":"#/components/schemas/InfraFitKarpenterChange"},"description":"Structured change set (always full, not capped by recommendationLimit)"}},"required":["type","hasChanges","currentConfig","recommendedConfig","changes"]},"InfraFitKarpenterChange":{"type":"object","description":"A single Karpenter configuration change recommendation","properties":{"id":{"type":"string","description":"Unique identifier for this change entry (matches path)"},"title":{"type":"string","description":"Human-readable change title"},"path":{"type":"string","description":"JSONPath (RFC 9535 syntax) into the NodePool CR. Nested keys and array equality-filter selectors are supported, e.g. \".spec.disruption.consolidationPolicy\" or \".spec.template.spec.requirements[?(@.key==\\\"node.kubernetes.io/instance-type\\\")]\".\n"},"operation":{"type":"string","description":"Change operation: \"replace\", \"merge\", or \"remove\". An empty operation is treated as \"replace\"; a replace with a null recommendedValue removes the field.\n"},"currentValue":{"description":"Current value at the path (string, number, boolean, array, object, or null)"},"recommendedValue":{"description":"Recommended value (string, number, boolean, array, object, or null)"},"rationale":{"type":"string","description":"Human-readable explanation for this change"}},"required":["id","title","path","operation","currentValue","recommendedValue","rationale"]},"InfraFitMeta":{"type":"object","description":"Response metadata for InfraFit endpoints","properties":{"timeframe":{"type":"string","description":"Analysis period echoed from the period query parameter (e.g. \"P30D\")"},"pagination":{"description":"Cursor-based pagination metadata. Present in list responses; omitted for single-item responses.","allOf":[{"$ref":"#/components/schemas/PaginationMeta"}]}},"required":["timeframe"]},"PaginationMeta":{"type":"object","description":"Cursor-based pagination metadata for list responses.","properties":{"next":{"type":["string","null"],"description":"Opaque cursor token for the next page, or null if this is the last page"},"prev":{"type":["string","null"],"description":"Opaque cursor token for the previous page, or null if this is the first page"},"pageSize":{"type":"integer","description":"Number of items per page used for this response"}},"required":["next","prev","pageSize"]},"Problem":{"type":"object","description":"RFC 9457 Problem Details error body, served as `application/problem+json`. `code` is a stable machine-readable identifier (e.g. `filter_key_not_supported`, `not_found`, `internal_error`); `retryable` indicates whether retrying the same request may succeed without client-side changes.\n","properties":{"type":{"type":"string","description":"URI reference identifying the problem type."},"title":{"type":"string","description":"Short, human-readable summary of the problem type."},"status":{"type":"integer","description":"HTTP status code generated by the origin server for this occurrence of the problem."},"detail":{"type":"string","description":"Human-readable explanation specific to this occurrence of the problem."},"instance":{"type":"string","description":"URI reference identifying the specific occurrence of the problem."},"code":{"type":"string","description":"Stable machine-readable error code."},"retryable":{"type":"boolean","description":"Whether retrying the same request may succeed without client-side changes."}},"required":["type","title","status","code","retryable"]}}},"paths":{"/clusters/{clusterUid}/node-groups/{nodeGroupName}":{"get":{"operationId":"GetInfraFitNodeGroup","tags":["InfraFit"],"summary":"Get a single node group with infra-fit data and recommendations","description":"Returns a single node group by name. Same shape as one item from the list endpoint. Returns 404 if the group does not exist in the cluster.\n","parameters":[{"name":"clusterUid","in":"path","required":true,"schema":{"type":"string"},"description":"Cluster unique identifier"},{"name":"nodeGroupName","in":"path","required":true,"schema":{"type":"string"},"description":"Node group name"},{"name":"period","in":"query","schema":{"allOf":[{"$ref":"#/components/schemas/InfraFitPeriod"}],"default":"P30D"},"description":"Window for the node group's own utilization/idle-cost metrics (CPU, memory, running minutes, unrecognized-group detection). Does not affect recommendation savings/pricing math, which always uses a fixed 30-day window regardless of this value. Default P30D.\n"},{"name":"recommendationLimit","in":"query","schema":{"type":"integer","default":3,"minimum":1,"maximum":20},"description":"Max recommended node types per group (Karpenter always returns full change set)"}],"responses":{"200":{"description":"Single node group","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/InfraFitNodeGroup"},"meta":{"$ref":"#/components/schemas/InfraFitMeta"}},"required":["data","meta"]}}}},"400":{"description":"Invalid parameters","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Cluster or node group not found","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal server error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}}}
```

## List unevictable pods in a cluster

> Returns the pods from the latest pre-computed unevictable-pods snapshot for the cluster (no request-time recompute). Tenant resolved from the auth token; a cluster belonging to another tenant returns 404.<br>

```json
{"openapi":"3.1.0","info":{"title":"Perfectscale API - OpenAPI 3.1","version":"v1"},"servers":[{"url":"https://api.app.perfectscale.io/public/v1"}],"security":[{"auth":[]}],"components":{"securitySchemes":{"auth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"UnevictablePod":{"type":"object","description":"A single unevictable pod from the latest pre-computed snapshot for the cluster. Returned both as a list item and as the single-pod detail response — the detail response additionally populates siblingPodNames.\n","required":["name","namespace","id","workload","reasons","phase","startTime"],"properties":{"name":{"type":"string"},"namespace":{"type":"string"},"id":{"type":"string"},"workload":{"$ref":"#/components/schemas/UnevictableWorkloadRef"},"reasons":{"type":"array","description":"Empty = pod is evictable; non-empty = unevictable.","items":{"$ref":"#/components/schemas/UnevictableReason"}},"phase":{"type":"string"},"startTime":{"type":"string","format":"date-time","description":"Pod creation timestamp (RFC 3339 UTC)."},"labels":{"type":"object","additionalProperties":{"type":"string"}},"annotations":{"type":"object","additionalProperties":{"type":"string"}},"spec":{"$ref":"#/components/schemas/UnevictablePodSpec"},"blockedNodeCount":{"type":"integer","description":"Number of nodes this pod blocks from scaling down."},"blockedNodes":{"type":"array","description":"Names of the nodes this pod pins.","items":{"type":"string"}},"blockedCostHourly":{"description":"Hourly cost of the blocked nodes (Total Blocked Cost). Filtering (`blockedCostHourly:gte:<amount>`) and sorting compare numerically on the underlying cost value, not the serialized `Money` object.\n","allOf":[{"$ref":"#/components/schemas/Money"}]},"clusterUid":{"type":"string"},"mute":{"type":"boolean","description":"True when the pod's workload is muted by a dismissal rule, muting all of its reasons."},"siblingPodNames":{"type":"array","description":"Names of other pods of the same workload id in this snapshot. Populated only by the single-pod detail endpoint.","items":{"type":"string"}}}},"UnevictableWorkloadRef":{"type":"object","description":"Identity of the pod's top-level owning workload.","required":["id","type"],"properties":{"id":{"type":"string","description":"Canonical workload id (namespace-kind-name) of the top-level controller."},"name":{"type":"string","description":"Denormalized workload name for display."},"type":{"type":"string","description":"Kubernetes workload kind (e.g. Deployment, StatefulSet, DaemonSet)."}}},"UnevictableReason":{"type":"object","required":["reason","details"],"properties":{"reason":{"type":"string","description":"Short reason code (e.g. \"Topology-Locked\", \"PDB Violation\")."},"reasonCode":{"oneOf":[{"$ref":"#/components/schemas/UnevictableReasonCode"},{"type":"null"}],"description":"Stable slug derived from the display reason. Null for unrecognised reasons."},"details":{"type":"string","description":"Human-readable explanation."},"remediation":{"$ref":"#/components/schemas/UnevictableRemediation"},"mute":{"type":"boolean","description":"True when this reason is covered by a dismissal rule and therefore muted."},"mutedByRule":{"oneOf":[{"$ref":"#/components/schemas/UnevictableMutedByRule"},{"type":"null"}]}}},"UnevictableReasonCode":{"type":"string","description":"Stable canonical identifier for an unevictable reason. Decoupled from the mutable display string.","enum":["opt_out_annotation","pod_disruption_budget","strict_affinity","strict_anti_affinity","topology_locked","taints_not_tolerated","local_node_storage","static_mirror_pod"]},"UnevictableRemediation":{"type":"object","description":"Reason-specific fix. current/recommended spec and the yaml diff are null for partial/informational cases.\n","required":["fixSummary","risk","confidence"],"properties":{"fixSummary":{"type":"string","description":"One-line plain-language fix."},"risk":{"type":"string","enum":["none","low","medium","high"]},"confidence":{"type":"string","enum":["low","medium","high"]},"currentSpec":{"type":["string","null"],"description":"Rendered YAML of the current spec; null for partial/informational cases."},"recommendedSpec":{"type":["string","null"],"description":"Rendered YAML of the recommended spec; null for partial/informational cases."},"yamlDiff":{"type":["string","null"],"description":"Unified diff string; null when there is no clean diff."}}},"UnevictableMutedByRule":{"type":"object","description":"Summary of the dismissal rule that suppressed this finding.","required":["createdBy","createTime"],"properties":{"note":{"type":["string","null"]},"createdBy":{"type":"string","description":"Email of the user who created the rule."},"createTime":{"type":"string","format":"date-time","description":"When the rule was created (RFC 3339 UTC)"}}},"UnevictablePodSpec":{"type":"object","description":"Raw Kubernetes PodSpec fields used by detection, verbatim from the pod.","properties":{"node":{"type":["string","null"],"description":"Node the pod is scheduled on (spec.nodeName); null for Pending pods."},"nodeGroup":{"type":"string","description":"Node group the pod's node belongs to; empty if the node's node group is unresolved."},"priority":{"type":"integer","description":"Kubernetes pod scheduling priority (spec.priority, from the pod's PriorityClass)."},"nodeSelector":{"type":"object","additionalProperties":{"type":"string"}},"affinity":{"allOf":[{"$ref":"#/components/schemas/UnevictablePodAffinity"}]},"tolerations":{"type":"array","items":{"$ref":"#/components/schemas/UnevictablePodToleration"}},"containers":{"type":"array","items":{"$ref":"#/components/schemas/UnevictablePodContainer"}},"volumes":{"type":"array","items":{"$ref":"#/components/schemas/UnevictablePodVolume"}},"topologySpreadConstraints":{"type":"array","items":{"$ref":"#/components/schemas/UnevictablePodTopologySpreadConstraint"}},"ownerReferences":{"type":"array","description":"The pod's metadata.ownerReferences, as the subset of fields described above. Omitted when the pod has no owner (e.g. static/mirror pods — see the static_mirror_pod reason code).\n","items":{"$ref":"#/components/schemas/UnevictablePodOwnerReference"}}}},"UnevictablePodAffinity":{"type":"object","description":"Simplified affinity block — only the sub-fields used by detection are modelled.","properties":{"nodeAffinity":{"type":"object","additionalProperties":true},"podAffinity":{"type":"object","additionalProperties":true},"podAntiAffinity":{"type":"object","additionalProperties":true}}},"UnevictablePodToleration":{"type":"object","required":["key","operator","effect"],"properties":{"key":{"type":"string"},"operator":{"type":"string"},"value":{"type":"string"},"effect":{"type":"string"}}},"UnevictablePodContainer":{"type":"object","required":["name","image"],"properties":{"name":{"type":"string"},"image":{"type":"string"},"cpuRequestCores":{"type":["number","null"],"format":"double","description":"CPU requested for the container, in cores; null if unset."},"cpuLimitCores":{"type":["number","null"],"format":"double","description":"CPU limit for the container, in cores; null if unset."},"memoryRequestMiB":{"type":["number","null"],"format":"double","description":"Memory requested for the container, in MiB; null if unset."},"memoryLimitMiB":{"type":["number","null"],"format":"double","description":"Memory limit for the container, in MiB; null if unset."},"gpuRequest":{"type":["integer","null"],"description":"GPU units requested for the container (e.g. nvidia.com/gpu); null if unset. GPU requests/limits are always whole units."},"gpuLimit":{"type":["integer","null"],"description":"GPU units the container is limited to (e.g. nvidia.com/gpu); null if unset. GPU requests/limits are always whole units."}}},"UnevictablePodVolume":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"hostPath":{"type":"string"},"emptyDir":{"type":"string"},"pvcClaimName":{"type":"string"}}},"UnevictablePodTopologySpreadConstraint":{"type":"object","required":["maxSkew","topologyKey","whenUnsatisfiable"],"properties":{"maxSkew":{"type":"integer","format":"int32"},"topologyKey":{"type":"string"},"whenUnsatisfiable":{"type":"string"},"labelSelector":{"type":"object","additionalProperties":true}}},"UnevictablePodOwnerReference":{"type":"object","description":"A subset of the pod's Kubernetes OwnerReference fields (apiVersion, kind, name, controller) used for ownership detection; not a complete copy of metadata.ownerReferences (e.g. uid is omitted).\n","required":["apiVersion","kind","name"],"properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"name":{"type":"string"},"controller":{"type":["boolean","null"],"description":"True when this reference is the pod's managing controller."}}},"Money":{"type":"object","description":"Monetary amount with currency","properties":{"amount":{"type":"string","description":"Decimal string representation of the amount (e.g. \"100.50\")"},"currency":{"type":"string","description":"ISO 4217 currency code (e.g. \"USD\")"}},"required":["amount","currency"]},"PaginationMeta":{"type":"object","description":"Cursor-based pagination metadata for list responses.","properties":{"next":{"type":["string","null"],"description":"Opaque cursor token for the next page, or null if this is the last page"},"prev":{"type":["string","null"],"description":"Opaque cursor token for the previous page, or null if this is the first page"},"pageSize":{"type":"integer","description":"Number of items per page used for this response"}},"required":["next","prev","pageSize"]},"UnevictableSummary":{"type":"object","description":"Aggregate pod/node counts for the snapshot backing the listing response.","required":["totalPods","unevictablePods","mute","totalNodes"],"properties":{"totalPods":{"type":"integer","description":"Total number of pods observed in the snapshot."},"unevictablePods":{"type":"integer","description":"Number of pods detected as unevictable in the snapshot."},"mute":{"type":"integer","description":"Number of unevictable findings muted by a dismissal rule in this snapshot."},"totalNodes":{"type":"integer","description":"Total number of nodes observed in the snapshot."},"autoscalerType":{"type":"string","description":"Detected autoscaler, rolled up from per-node detection (canonical\nvalues, e.g. \"cluster_autoscaler\", \"karpenter\", \"castai\",\n\"eks_auto_mode\"). Omitted if not detected. Results written before\nPSD-9016 may carry the legacy hyphenated \"cluster-autoscaler\"."}}},"UnevictablePodsStatus":{"type":"object","description":"Snapshot processing status, returned on 202 (still processing) and 422 (processing failed) instead of pod/report data.\n","properties":{"status":{"type":"string","enum":["processing","failed"]}},"required":["status"]},"Problem":{"type":"object","description":"RFC 9457 Problem Details error body, served as `application/problem+json`. `code` is a stable machine-readable identifier (e.g. `filter_key_not_supported`, `not_found`, `internal_error`); `retryable` indicates whether retrying the same request may succeed without client-side changes.\n","properties":{"type":{"type":"string","description":"URI reference identifying the problem type."},"title":{"type":"string","description":"Short, human-readable summary of the problem type."},"status":{"type":"integer","description":"HTTP status code generated by the origin server for this occurrence of the problem."},"detail":{"type":"string","description":"Human-readable explanation specific to this occurrence of the problem."},"instance":{"type":"string","description":"URI reference identifying the specific occurrence of the problem."},"code":{"type":"string","description":"Stable machine-readable error code."},"retryable":{"type":"boolean","description":"Whether retrying the same request may succeed without client-side changes."}},"required":["type","title","status","code","retryable"]}}},"paths":{"/clusters/{clusterUid}/unevictable-pods":{"get":{"operationId":"ListUnevictablePods","tags":["Unevictable"],"summary":"List unevictable pods in a cluster","description":"Returns the pods from the latest pre-computed unevictable-pods snapshot for the cluster (no request-time recompute). Tenant resolved from the auth token; a cluster belonging to another tenant returns 404.\n","parameters":[{"name":"clusterUid","in":"path","required":true,"schema":{"type":"string"},"description":"Cluster unique identifier"},{"name":"filter","in":"query","required":false,"schema":{"type":"string"},"description":"Composite filter expression: clauses of the form `key[:op]:value` joined by `|` (AND); `op` is optional and defaults to `eq`; comma-separate values within a clause for an IN match, e.g. `filter=namespace:payments|blockedCostHourly:gte:10`. Supported keys for this endpoint: `namespace` (eq), `blockedCostHourly` (eq/gte/lte, compares numerically on the underlying cost value, not the serialized `Money` object), `reasonCode` (eq), `nodeGroup` (eq), `labels.<key>` (eq). `labels.<key>` matches against the pod's Kubernetes labels — for example `filter=labels.team:payments` matches pods labeled `team=payments`. An unsupported key returns 400 with `code: \"filter_key_not_supported\"`. Mute-status filtering is handled by the separate `mute` parameter, not this filter.\n"},{"name":"mute","in":"query","required":false,"schema":{"type":"string","enum":["exclude","include","only"],"default":"exclude"},"description":"Muted-finding handling: `exclude` (default) hides pods whose workload has a dismissal rule; `include` returns all pods, flagging muted ones via `mute`/`mutedByRule` on each reason; `only` returns exclusively muted pods, for auditing what's been silenced.\n"},{"name":"sortBy","in":"query","required":false,"schema":{"type":"string","enum":["blockedCostHourly"]},"description":"Field to sort results by. `blockedCostHourly` sorts numerically on the underlying cost value, not the serialized `Money` object. Unset falls back to a stable but unspecified order (currently pod UID). Ties are always broken by UID for stable pagination.\n"},{"name":"sortOrder","in":"query","required":false,"schema":{"type":"string","enum":["asc","desc"],"default":"desc"},"description":"Sort direction, applied only when sortBy is set."},{"name":"pageSize","in":"query","schema":{"type":"integer","default":50,"minimum":1,"maximum":500},"description":"Number of items per page (default 50, max 500)"},{"name":"pageToken","in":"query","schema":{"type":"string"},"description":"Opaque cursor token for pagination"}],"responses":{"200":{"description":"List of unevictable pods","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/UnevictablePod"}},"meta":{"type":"object","properties":{"pagination":{"$ref":"#/components/schemas/PaginationMeta"},"snapshotTime":{"type":"string","format":"date-time","description":"Timestamp (RFC 3339, UTC) of the snapshot the data was read from"},"algorithmVersion":{"type":"string","description":"Version of the unevictable-detection algorithm/library used to produce the snapshot"},"summary":{"$ref":"#/components/schemas/UnevictableSummary"}},"required":["pagination","snapshotTime","algorithmVersion","summary"]}},"required":["data","meta"]}}}},"202":{"description":"Raw snapshot exists but is still being processed","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/UnevictablePodsStatus"}},"required":["data"]}}}},"400":{"description":"Invalid parameters","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Cluster or snapshot not found","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Raw snapshot exists but processing failed","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal server error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}}}
```

## Per-pod unevictable report — one row per pod, with all its reasons combined

> Issues view — one row per pod, carrying all of that pod's unevictable reasons. Served from the latest pre-computed snapshot (no request-time recompute). Tenant resolved from the auth token; a cluster belonging to another tenant returns 404.<br>

```json
{"openapi":"3.1.0","info":{"title":"Perfectscale API - OpenAPI 3.1","version":"v1"},"servers":[{"url":"https://api.app.perfectscale.io/public/v1"}],"security":[{"auth":[]}],"components":{"securitySchemes":{"auth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"UnevictableReportRow":{"type":"object","description":"One row of the per-pod issues view — a single unevictable pod with all its reasons combined.","required":["name","id","workload","namespace","reasons","mute"],"properties":{"name":{"type":"string"},"id":{"type":"string"},"workload":{"$ref":"#/components/schemas/UnevictableWorkloadRef"},"namespace":{"type":"string"},"labels":{"type":"object","additionalProperties":{"type":"string"}},"node":{"type":["string","null"],"description":"Node the pod is scheduled on; null for Pending pods."},"nodeGroup":{"type":"string","description":"Node group the pod's node belongs to; empty if the node's node group is unresolved."},"reasons":{"type":"array","description":"All unevictable reasons for this pod.","items":{"$ref":"#/components/schemas/UnevictableReason"}},"mute":{"type":"boolean","description":"True when the pod's workload is muted by a dismissal rule, muting all of its reasons."},"priority":{"type":"integer","description":"Kubernetes pod scheduling priority (from the pod's PriorityClass); same value as UnevictablePod.spec.priority."},"blockedCostHourly":{"description":"Hourly cost of the blocked nodes. Filtering (`blockedCostHourly:gte:<amount>`) and sorting compare numerically on the underlying cost value, not the serialized `Money` object.\n","allOf":[{"$ref":"#/components/schemas/Money"}]}}},"UnevictableWorkloadRef":{"type":"object","description":"Identity of the pod's top-level owning workload.","required":["id","type"],"properties":{"id":{"type":"string","description":"Canonical workload id (namespace-kind-name) of the top-level controller."},"name":{"type":"string","description":"Denormalized workload name for display."},"type":{"type":"string","description":"Kubernetes workload kind (e.g. Deployment, StatefulSet, DaemonSet)."}}},"UnevictableReason":{"type":"object","required":["reason","details"],"properties":{"reason":{"type":"string","description":"Short reason code (e.g. \"Topology-Locked\", \"PDB Violation\")."},"reasonCode":{"oneOf":[{"$ref":"#/components/schemas/UnevictableReasonCode"},{"type":"null"}],"description":"Stable slug derived from the display reason. Null for unrecognised reasons."},"details":{"type":"string","description":"Human-readable explanation."},"remediation":{"$ref":"#/components/schemas/UnevictableRemediation"},"mute":{"type":"boolean","description":"True when this reason is covered by a dismissal rule and therefore muted."},"mutedByRule":{"oneOf":[{"$ref":"#/components/schemas/UnevictableMutedByRule"},{"type":"null"}]}}},"UnevictableReasonCode":{"type":"string","description":"Stable canonical identifier for an unevictable reason. Decoupled from the mutable display string.","enum":["opt_out_annotation","pod_disruption_budget","strict_affinity","strict_anti_affinity","topology_locked","taints_not_tolerated","local_node_storage","static_mirror_pod"]},"UnevictableRemediation":{"type":"object","description":"Reason-specific fix. current/recommended spec and the yaml diff are null for partial/informational cases.\n","required":["fixSummary","risk","confidence"],"properties":{"fixSummary":{"type":"string","description":"One-line plain-language fix."},"risk":{"type":"string","enum":["none","low","medium","high"]},"confidence":{"type":"string","enum":["low","medium","high"]},"currentSpec":{"type":["string","null"],"description":"Rendered YAML of the current spec; null for partial/informational cases."},"recommendedSpec":{"type":["string","null"],"description":"Rendered YAML of the recommended spec; null for partial/informational cases."},"yamlDiff":{"type":["string","null"],"description":"Unified diff string; null when there is no clean diff."}}},"UnevictableMutedByRule":{"type":"object","description":"Summary of the dismissal rule that suppressed this finding.","required":["createdBy","createTime"],"properties":{"note":{"type":["string","null"]},"createdBy":{"type":"string","description":"Email of the user who created the rule."},"createTime":{"type":"string","format":"date-time","description":"When the rule was created (RFC 3339 UTC)"}}},"Money":{"type":"object","description":"Monetary amount with currency","properties":{"amount":{"type":"string","description":"Decimal string representation of the amount (e.g. \"100.50\")"},"currency":{"type":"string","description":"ISO 4217 currency code (e.g. \"USD\")"}},"required":["amount","currency"]},"PaginationMeta":{"type":"object","description":"Cursor-based pagination metadata for list responses.","properties":{"next":{"type":["string","null"],"description":"Opaque cursor token for the next page, or null if this is the last page"},"prev":{"type":["string","null"],"description":"Opaque cursor token for the previous page, or null if this is the first page"},"pageSize":{"type":"integer","description":"Number of items per page used for this response"}},"required":["next","prev","pageSize"]},"UnevictableSummary":{"type":"object","description":"Aggregate pod/node counts for the snapshot backing the listing response.","required":["totalPods","unevictablePods","mute","totalNodes"],"properties":{"totalPods":{"type":"integer","description":"Total number of pods observed in the snapshot."},"unevictablePods":{"type":"integer","description":"Number of pods detected as unevictable in the snapshot."},"mute":{"type":"integer","description":"Number of unevictable findings muted by a dismissal rule in this snapshot."},"totalNodes":{"type":"integer","description":"Total number of nodes observed in the snapshot."},"autoscalerType":{"type":"string","description":"Detected autoscaler, rolled up from per-node detection (canonical\nvalues, e.g. \"cluster_autoscaler\", \"karpenter\", \"castai\",\n\"eks_auto_mode\"). Omitted if not detected. Results written before\nPSD-9016 may carry the legacy hyphenated \"cluster-autoscaler\"."}}},"UnevictablePodsStatus":{"type":"object","description":"Snapshot processing status, returned on 202 (still processing) and 422 (processing failed) instead of pod/report data.\n","properties":{"status":{"type":"string","enum":["processing","failed"]}},"required":["status"]},"Problem":{"type":"object","description":"RFC 9457 Problem Details error body, served as `application/problem+json`. `code` is a stable machine-readable identifier (e.g. `filter_key_not_supported`, `not_found`, `internal_error`); `retryable` indicates whether retrying the same request may succeed without client-side changes.\n","properties":{"type":{"type":"string","description":"URI reference identifying the problem type."},"title":{"type":"string","description":"Short, human-readable summary of the problem type."},"status":{"type":"integer","description":"HTTP status code generated by the origin server for this occurrence of the problem."},"detail":{"type":"string","description":"Human-readable explanation specific to this occurrence of the problem."},"instance":{"type":"string","description":"URI reference identifying the specific occurrence of the problem."},"code":{"type":"string","description":"Stable machine-readable error code."},"retryable":{"type":"boolean","description":"Whether retrying the same request may succeed without client-side changes."}},"required":["type","title","status","code","retryable"]}}},"paths":{"/clusters/{clusterUid}/unevictable-report":{"get":{"operationId":"GetUnevictableReport","tags":["Unevictable"],"summary":"Per-pod unevictable report — one row per pod, with all its reasons combined","description":"Issues view — one row per pod, carrying all of that pod's unevictable reasons. Served from the latest pre-computed snapshot (no request-time recompute). Tenant resolved from the auth token; a cluster belonging to another tenant returns 404.\n","parameters":[{"name":"clusterUid","in":"path","required":true,"schema":{"type":"string"},"description":"Cluster unique identifier"},{"name":"filter","in":"query","required":false,"schema":{"type":"string"},"description":"Composite filter expression: clauses of the form `key[:op]:value` joined by `|` (AND); `op` is optional and defaults to `eq`; comma-separate values within a clause for an IN match. Supported keys for this endpoint: `namespace` (eq), `blockedCostHourly` (eq/gte/lte, compares numerically on the underlying cost value, not the serialized `Money` object), `nodeGroup` (eq), `labels.<key>` (eq). `labels.<key>` matches against the pod's Kubernetes labels — for example `filter=labels.team:payments` matches pods labeled `team=payments`. An unsupported key returns 400 with `code: \"filter_key_not_supported\"`. Mute-status filtering is handled by the separate `mute` parameter, not this filter.\n"},{"name":"mute","in":"query","required":false,"schema":{"type":"string","enum":["exclude","include","only"],"default":"exclude"},"description":"Muted-finding handling: `exclude` (default) hides rows whose workload has a dismissal rule; `include` returns all rows, flagged via `mute`; `only` returns exclusively muted rows, for auditing what's been silenced.\n"},{"name":"sortBy","in":"query","required":false,"schema":{"type":"string","enum":["blockedCostHourly"]},"description":"Field to sort results by. `blockedCostHourly` sorts numerically on the underlying cost value, not the serialized `Money` object. Unset falls back to a stable but unspecified order (currently pod UID). Ties are always broken by UID for stable pagination.\n"},{"name":"sortOrder","in":"query","required":false,"schema":{"type":"string","enum":["asc","desc"],"default":"desc"},"description":"Sort direction, applied only when sortBy is set."},{"name":"pageSize","in":"query","schema":{"type":"integer","default":50,"minimum":1,"maximum":500},"description":"Number of items per page (default 50, max 500)"},{"name":"pageToken","in":"query","schema":{"type":"string"},"description":"Opaque cursor token for pagination"}],"responses":{"200":{"description":"List of unevictable pod issue rows","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/UnevictableReportRow"}},"meta":{"type":"object","properties":{"pagination":{"$ref":"#/components/schemas/PaginationMeta"},"snapshotTime":{"type":"string","format":"date-time","description":"Timestamp (RFC 3339, UTC) of the snapshot the data was read from"},"algorithmVersion":{"type":"string","description":"Version of the unevictable-detection algorithm/library used to produce the snapshot"},"summary":{"$ref":"#/components/schemas/UnevictableSummary"}},"required":["pagination","snapshotTime","algorithmVersion","summary"]}},"required":["data","meta"]}}}},"202":{"description":"Raw snapshot exists but is still being processed","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/UnevictablePodsStatus"}},"required":["data"]}}}},"400":{"description":"Invalid parameters","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Cluster or snapshot not found","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Raw snapshot exists but processing failed","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal server error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}}}
```

## Get a single unevictable pod

> Full detail for one pod (all reasons with the full remediation object), served from the latest pre-computed snapshot (no request-time recompute). Tenant resolved from the auth token; a cluster belonging to another tenant, or a pod not present in the latest snapshot, returns 404.<br>

```json
{"openapi":"3.1.0","info":{"title":"Perfectscale API - OpenAPI 3.1","version":"v1"},"servers":[{"url":"https://api.app.perfectscale.io/public/v1"}],"security":[{"auth":[]}],"components":{"securitySchemes":{"auth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"UnevictablePod":{"type":"object","description":"A single unevictable pod from the latest pre-computed snapshot for the cluster. Returned both as a list item and as the single-pod detail response — the detail response additionally populates siblingPodNames.\n","required":["name","namespace","id","workload","reasons","phase","startTime"],"properties":{"name":{"type":"string"},"namespace":{"type":"string"},"id":{"type":"string"},"workload":{"$ref":"#/components/schemas/UnevictableWorkloadRef"},"reasons":{"type":"array","description":"Empty = pod is evictable; non-empty = unevictable.","items":{"$ref":"#/components/schemas/UnevictableReason"}},"phase":{"type":"string"},"startTime":{"type":"string","format":"date-time","description":"Pod creation timestamp (RFC 3339 UTC)."},"labels":{"type":"object","additionalProperties":{"type":"string"}},"annotations":{"type":"object","additionalProperties":{"type":"string"}},"spec":{"$ref":"#/components/schemas/UnevictablePodSpec"},"blockedNodeCount":{"type":"integer","description":"Number of nodes this pod blocks from scaling down."},"blockedNodes":{"type":"array","description":"Names of the nodes this pod pins.","items":{"type":"string"}},"blockedCostHourly":{"description":"Hourly cost of the blocked nodes (Total Blocked Cost). Filtering (`blockedCostHourly:gte:<amount>`) and sorting compare numerically on the underlying cost value, not the serialized `Money` object.\n","allOf":[{"$ref":"#/components/schemas/Money"}]},"clusterUid":{"type":"string"},"mute":{"type":"boolean","description":"True when the pod's workload is muted by a dismissal rule, muting all of its reasons."},"siblingPodNames":{"type":"array","description":"Names of other pods of the same workload id in this snapshot. Populated only by the single-pod detail endpoint.","items":{"type":"string"}}}},"UnevictableWorkloadRef":{"type":"object","description":"Identity of the pod's top-level owning workload.","required":["id","type"],"properties":{"id":{"type":"string","description":"Canonical workload id (namespace-kind-name) of the top-level controller."},"name":{"type":"string","description":"Denormalized workload name for display."},"type":{"type":"string","description":"Kubernetes workload kind (e.g. Deployment, StatefulSet, DaemonSet)."}}},"UnevictableReason":{"type":"object","required":["reason","details"],"properties":{"reason":{"type":"string","description":"Short reason code (e.g. \"Topology-Locked\", \"PDB Violation\")."},"reasonCode":{"oneOf":[{"$ref":"#/components/schemas/UnevictableReasonCode"},{"type":"null"}],"description":"Stable slug derived from the display reason. Null for unrecognised reasons."},"details":{"type":"string","description":"Human-readable explanation."},"remediation":{"$ref":"#/components/schemas/UnevictableRemediation"},"mute":{"type":"boolean","description":"True when this reason is covered by a dismissal rule and therefore muted."},"mutedByRule":{"oneOf":[{"$ref":"#/components/schemas/UnevictableMutedByRule"},{"type":"null"}]}}},"UnevictableReasonCode":{"type":"string","description":"Stable canonical identifier for an unevictable reason. Decoupled from the mutable display string.","enum":["opt_out_annotation","pod_disruption_budget","strict_affinity","strict_anti_affinity","topology_locked","taints_not_tolerated","local_node_storage","static_mirror_pod"]},"UnevictableRemediation":{"type":"object","description":"Reason-specific fix. current/recommended spec and the yaml diff are null for partial/informational cases.\n","required":["fixSummary","risk","confidence"],"properties":{"fixSummary":{"type":"string","description":"One-line plain-language fix."},"risk":{"type":"string","enum":["none","low","medium","high"]},"confidence":{"type":"string","enum":["low","medium","high"]},"currentSpec":{"type":["string","null"],"description":"Rendered YAML of the current spec; null for partial/informational cases."},"recommendedSpec":{"type":["string","null"],"description":"Rendered YAML of the recommended spec; null for partial/informational cases."},"yamlDiff":{"type":["string","null"],"description":"Unified diff string; null when there is no clean diff."}}},"UnevictableMutedByRule":{"type":"object","description":"Summary of the dismissal rule that suppressed this finding.","required":["createdBy","createTime"],"properties":{"note":{"type":["string","null"]},"createdBy":{"type":"string","description":"Email of the user who created the rule."},"createTime":{"type":"string","format":"date-time","description":"When the rule was created (RFC 3339 UTC)"}}},"UnevictablePodSpec":{"type":"object","description":"Raw Kubernetes PodSpec fields used by detection, verbatim from the pod.","properties":{"node":{"type":["string","null"],"description":"Node the pod is scheduled on (spec.nodeName); null for Pending pods."},"nodeGroup":{"type":"string","description":"Node group the pod's node belongs to; empty if the node's node group is unresolved."},"priority":{"type":"integer","description":"Kubernetes pod scheduling priority (spec.priority, from the pod's PriorityClass)."},"nodeSelector":{"type":"object","additionalProperties":{"type":"string"}},"affinity":{"allOf":[{"$ref":"#/components/schemas/UnevictablePodAffinity"}]},"tolerations":{"type":"array","items":{"$ref":"#/components/schemas/UnevictablePodToleration"}},"containers":{"type":"array","items":{"$ref":"#/components/schemas/UnevictablePodContainer"}},"volumes":{"type":"array","items":{"$ref":"#/components/schemas/UnevictablePodVolume"}},"topologySpreadConstraints":{"type":"array","items":{"$ref":"#/components/schemas/UnevictablePodTopologySpreadConstraint"}},"ownerReferences":{"type":"array","description":"The pod's metadata.ownerReferences, as the subset of fields described above. Omitted when the pod has no owner (e.g. static/mirror pods — see the static_mirror_pod reason code).\n","items":{"$ref":"#/components/schemas/UnevictablePodOwnerReference"}}}},"UnevictablePodAffinity":{"type":"object","description":"Simplified affinity block — only the sub-fields used by detection are modelled.","properties":{"nodeAffinity":{"type":"object","additionalProperties":true},"podAffinity":{"type":"object","additionalProperties":true},"podAntiAffinity":{"type":"object","additionalProperties":true}}},"UnevictablePodToleration":{"type":"object","required":["key","operator","effect"],"properties":{"key":{"type":"string"},"operator":{"type":"string"},"value":{"type":"string"},"effect":{"type":"string"}}},"UnevictablePodContainer":{"type":"object","required":["name","image"],"properties":{"name":{"type":"string"},"image":{"type":"string"},"cpuRequestCores":{"type":["number","null"],"format":"double","description":"CPU requested for the container, in cores; null if unset."},"cpuLimitCores":{"type":["number","null"],"format":"double","description":"CPU limit for the container, in cores; null if unset."},"memoryRequestMiB":{"type":["number","null"],"format":"double","description":"Memory requested for the container, in MiB; null if unset."},"memoryLimitMiB":{"type":["number","null"],"format":"double","description":"Memory limit for the container, in MiB; null if unset."},"gpuRequest":{"type":["integer","null"],"description":"GPU units requested for the container (e.g. nvidia.com/gpu); null if unset. GPU requests/limits are always whole units."},"gpuLimit":{"type":["integer","null"],"description":"GPU units the container is limited to (e.g. nvidia.com/gpu); null if unset. GPU requests/limits are always whole units."}}},"UnevictablePodVolume":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"hostPath":{"type":"string"},"emptyDir":{"type":"string"},"pvcClaimName":{"type":"string"}}},"UnevictablePodTopologySpreadConstraint":{"type":"object","required":["maxSkew","topologyKey","whenUnsatisfiable"],"properties":{"maxSkew":{"type":"integer","format":"int32"},"topologyKey":{"type":"string"},"whenUnsatisfiable":{"type":"string"},"labelSelector":{"type":"object","additionalProperties":true}}},"UnevictablePodOwnerReference":{"type":"object","description":"A subset of the pod's Kubernetes OwnerReference fields (apiVersion, kind, name, controller) used for ownership detection; not a complete copy of metadata.ownerReferences (e.g. uid is omitted).\n","required":["apiVersion","kind","name"],"properties":{"apiVersion":{"type":"string"},"kind":{"type":"string"},"name":{"type":"string"},"controller":{"type":["boolean","null"],"description":"True when this reference is the pod's managing controller."}}},"Money":{"type":"object","description":"Monetary amount with currency","properties":{"amount":{"type":"string","description":"Decimal string representation of the amount (e.g. \"100.50\")"},"currency":{"type":"string","description":"ISO 4217 currency code (e.g. \"USD\")"}},"required":["amount","currency"]},"UnevictablePodsStatus":{"type":"object","description":"Snapshot processing status, returned on 202 (still processing) and 422 (processing failed) instead of pod/report data.\n","properties":{"status":{"type":"string","enum":["processing","failed"]}},"required":["status"]},"Problem":{"type":"object","description":"RFC 9457 Problem Details error body, served as `application/problem+json`. `code` is a stable machine-readable identifier (e.g. `filter_key_not_supported`, `not_found`, `internal_error`); `retryable` indicates whether retrying the same request may succeed without client-side changes.\n","properties":{"type":{"type":"string","description":"URI reference identifying the problem type."},"title":{"type":"string","description":"Short, human-readable summary of the problem type."},"status":{"type":"integer","description":"HTTP status code generated by the origin server for this occurrence of the problem."},"detail":{"type":"string","description":"Human-readable explanation specific to this occurrence of the problem."},"instance":{"type":"string","description":"URI reference identifying the specific occurrence of the problem."},"code":{"type":"string","description":"Stable machine-readable error code."},"retryable":{"type":"boolean","description":"Whether retrying the same request may succeed without client-side changes."}},"required":["type","title","status","code","retryable"]}}},"paths":{"/clusters/{clusterUid}/unevictable-pods/{podUid}":{"get":{"operationId":"GetUnevictablePod","tags":["Unevictable"],"summary":"Get a single unevictable pod","description":"Full detail for one pod (all reasons with the full remediation object), served from the latest pre-computed snapshot (no request-time recompute). Tenant resolved from the auth token; a cluster belonging to another tenant, or a pod not present in the latest snapshot, returns 404.\n","parameters":[{"name":"clusterUid","in":"path","required":true,"schema":{"type":"string"},"description":"Cluster unique identifier"},{"name":"podUid","in":"path","required":true,"schema":{"type":"string"},"description":"Pod unique identifier"}],"responses":{"200":{"description":"Single unevictable pod, returned directly (no envelope)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnevictablePod"}}}},"202":{"description":"Raw snapshot exists but is still being processed, returned directly (no envelope)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnevictablePodsStatus"}}}},"404":{"description":"Cluster or snapshot not found, or no pod with this uid in the latest snapshot","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Raw snapshot exists but processing failed","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal server error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}}}
```

## List unevictable muted workloads for a cluster

> Read-only list of workloads that have an active mute (dismissal) rule for the cluster. Rules are created and removed via the web app / user API only; this endpoint does not expose write operations. Tenant resolved from the auth token; a cluster belonging to another tenant returns 404.<br>

```json
{"openapi":"3.1.0","info":{"title":"Perfectscale API - OpenAPI 3.1","version":"v1"},"servers":[{"url":"https://api.app.perfectscale.io/public/v1"}],"security":[{"auth":[]}],"components":{"securitySchemes":{"auth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"UnevictableMutedWorkload":{"type":"object","required":["clusterUid","id","createdBy","createTime","updateTime"],"properties":{"clusterUid":{"type":"string"},"id":{"type":"string","description":"Canonical workload id (namespace-kind-name) of the top-level controller."},"namespace":{"type":"string"},"workloadName":{"type":"string","description":"Denormalized workload name for display."},"note":{"type":"string"},"createdBy":{"type":"string","description":"Email of the user who created the rule."},"createTime":{"type":"string","format":"date-time","description":"When the rule was created (RFC 3339 UTC)"},"updateTime":{"type":"string","format":"date-time","description":"When the rule was last updated (RFC 3339 UTC)"}}},"PaginationMeta":{"type":"object","description":"Cursor-based pagination metadata for list responses.","properties":{"next":{"type":["string","null"],"description":"Opaque cursor token for the next page, or null if this is the last page"},"prev":{"type":["string","null"],"description":"Opaque cursor token for the previous page, or null if this is the first page"},"pageSize":{"type":"integer","description":"Number of items per page used for this response"}},"required":["next","prev","pageSize"]},"Problem":{"type":"object","description":"RFC 9457 Problem Details error body, served as `application/problem+json`. `code` is a stable machine-readable identifier (e.g. `filter_key_not_supported`, `not_found`, `internal_error`); `retryable` indicates whether retrying the same request may succeed without client-side changes.\n","properties":{"type":{"type":"string","description":"URI reference identifying the problem type."},"title":{"type":"string","description":"Short, human-readable summary of the problem type."},"status":{"type":"integer","description":"HTTP status code generated by the origin server for this occurrence of the problem."},"detail":{"type":"string","description":"Human-readable explanation specific to this occurrence of the problem."},"instance":{"type":"string","description":"URI reference identifying the specific occurrence of the problem."},"code":{"type":"string","description":"Stable machine-readable error code."},"retryable":{"type":"boolean","description":"Whether retrying the same request may succeed without client-side changes."}},"required":["type","title","status","code","retryable"]}}},"paths":{"/clusters/{clusterUid}/unevictable-muted-workloads":{"get":{"operationId":"ListUnevictableMutedWorkloads","tags":["Unevictable"],"summary":"List unevictable muted workloads for a cluster","description":"Read-only list of workloads that have an active mute (dismissal) rule for the cluster. Rules are created and removed via the web app / user API only; this endpoint does not expose write operations. Tenant resolved from the auth token; a cluster belonging to another tenant returns 404.\n","parameters":[{"name":"clusterUid","in":"path","required":true,"schema":{"type":"string"},"description":"Cluster unique identifier"},{"name":"pageSize","in":"query","schema":{"type":"integer","default":50,"minimum":1,"maximum":500},"description":"Number of items per page (default 50, max 500)"},{"name":"pageToken","in":"query","schema":{"type":"string"},"description":"Opaque cursor token for pagination"}],"responses":{"200":{"description":"List of muted workloads for the cluster","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/UnevictableMutedWorkload"}},"meta":{"type":"object","properties":{"pagination":{"$ref":"#/components/schemas/PaginationMeta"}},"required":["pagination"]}},"required":["data","meta"]}}}},"400":{"description":"Invalid parameters","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"description":"Cluster not found","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"500":{"description":"Internal server error","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}}}}
```

#### Infrafit GitHub actions

The InfraFit GitHub Action turns PerfectScale Karpenter NodePool recommendations into ready-to-review pull requests. It fetches recommendations from the PerfectScale API, maps them to the matching Karpenter YAML files in your repository, applies the changes, and opens a PR with the diff. You can create one PR per cluster or combine changes into a single PR.

{% hint style="info" %}
If a recommendation cannot be applied automatically, the action creates a GitHub issue instead, so no recommendation is missed.
{% endhint %}

The action never changes a live cluster. All changes are proposed through pull requests, and your existing review and approval process remains the control point.

How it helps:

* Reduces manual work. Recommendations are delivered as ready-to-review YAML changes instead of requiring engineers to update files by hand.
* Keeps recommendations actionable. Changes appear directly in GitHub, where engineering teams already manage infrastructure work.
* Fits GitOps workflows. Every change is reviewable, attributable, and recorded in git history. Engineers can approve, edit, or close each PR.
* Requires no new infrastructure. The action runs on existing GitHub Actions runners and supports Anthropic, OpenAI, Amazon Bedrock, GitHub Copilot, Azure OpenAI, and OpenAI-compatible endpoints.
* Keeps configuration aligned over time. Run the action on a schedule to continuously update Karpenter configuration as workload behavior changes.

{% hint style="info" %}
Access more information about Infrafit GitHub actions [here](https://github.com/perfectscale-io/infrafit-action).
{% endhint %}

### How to get cluster\_uid

There are two options available to obtain the **cluster\_uid**.

1. Get **cluster\_uid** directly from PerfectScale UI:
   * Go to the **`Overview`** tab
   * Click the three-dot button located next to the needed cluster<br>

     <figure><img src="/files/UIqLWKOTzDhUdTCjjksx" alt="" width="280"><figcaption><p>Get cluster UID</p></figcaption></figure>
   * Click the **`Copy Cluster UID`** button -> now, your cluster\_uid is copied.<br>

     <figure><img src="/files/bmXkmo77siAC6bRXkrkE" alt="" width="267"><figcaption><p>Cluster UID copied</p></figcaption></figure>
2. Get cluster\_uid with the following command:

```
kubectl get namespace kube-system -o=jsonpath='{.metadata.uid}'
```

3. Get the full list of clusters via [API](#clusters-endpoint) and extract cluster\_uids from it.

## Automation Audit Log Endpoint

The Automation Audit Log provides visibility into all actions performed by PerfectScale automation. This endpoint allows teams to programmatically access, retrieve, and filter audit logs and seamlessly integrate them into their existing systems or tools.

{% hint style="info" %}
The Automation Audit Log shows data from the last **30 days**.
{% endhint %}

## Get Automation Audit Logs with Cursor Pagination

> Retrieves a list of automation audit log entries within a specified time range, using cursor-based pagination.<br>

```json
{"openapi":"3.0.3","info":{"title":"Perfectscale API - OpenAPI 3.0","version":"v1"},"servers":[{"url":"https://api.app.perfectscale.io/public/v1"}],"security":[{"auth":[]}],"components":{"securitySchemes":{"auth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"AutomationLogEntry":{"type":"object","properties":{"started_at":{"type":"string","format":"date-time","description":"Timestamp when the action started."},"cluster_name":{"type":"string","description":"Name of the cluster."},"cluster_uid":{"type":"string","description":"ID of the cluster."},"workload_id":{"type":"string","description":"ID of the workload."},"workload_name":{"type":"string","description":"Name of the workload."},"workload_type":{"type":"string","description":"Type of deployment."},"namespace":{"type":"string","description":"Namespace of the workload."},"labels":{"type":"object","additionalProperties":{"type":"string"},"description":"Labels associated with the workload."},"container":{"$ref":"#/components/schemas/AutomatedLogsContainer"},"executed":{"$ref":"#/components/schemas/ExecutionType"},"workload_indicators":{"type":"array","description":"Workload-level indicators addressed by the automation change.","items":{"$ref":"#/components/schemas/Indicator"}}},"required":["started_at","cluster_name","cluster_uid","workload_id","workload_name","workload_type","namespace","labels","container","executed"]},"AutomatedLogsContainer":{"type":"object","description":"Properties of the container.","properties":{"name":{"type":"string","description":"Name of the container."},"cpu":{"$ref":"#/components/schemas/AutomatedLogsContainerCpu"},"memory":{"$ref":"#/components/schemas/AutomatedLogsContainerMemory"},"indicators":{"type":"array","description":"Container-level indicators addressed by the automation change.","items":{"$ref":"#/components/schemas/Indicator"}}},"required":["name","cpu","memory"]},"AutomatedLogsContainerCpu":{"type":"object","description":"CPU related properties.","properties":{"cpuCoresRequest":{"type":"integer","format":"int64","description":"Request of cpu before apply of automation."},"recommendCpuCoresRequest":{"type":"integer","format":"int64","description":"Request of cpu after apply of automation."},"cpuCoresLimits":{"type":"integer","format":"int64","description":"Limit of cpu before apply of automation."},"recommendCpuCoresLimits":{"type":"integer","format":"int64","description":"Limit of cpu after apply of automation."},"cpuRequestImpact":{"type":"integer","format":"int64","description":"Impact of automation on cpu request value."},"cpuLimitImpact":{"type":"integer","format":"int64","description":"Impact of automation on cpu limit value."},"cpuRequestChangePercent":{"type":"number","format":"double","description":"Single instance cpu request change in %."},"cpuLimitChangePercent":{"type":"number","format":"double","description":"Single instance cpu limit change in %."},"cpuRequestChangeAbsolute":{"type":"integer","format":"int64","description":"Single instance cpu request absolute value change."},"cpuLimitChangeAbsolute":{"type":"integer","format":"int64","description":"Single instance cpu limit absolute value change."}},"required":["cpuCoresRequest","recommendCpuCoresRequest","cpuCoresLimits","recommendCpuCoresLimits","cpuRequestImpact","cpuLimitImpact","cpuRequestChangePercent","cpuLimitChangePercent","cpuRequestChangeAbsolute","cpuLimitChangeAbsolute"]},"AutomatedLogsContainerMemory":{"type":"object","description":"Memory related properties.","properties":{"memMiBRequest":{"type":"integer","format":"int64","description":"Request of memory before apply of automation."},"recommendMemMiBRequest":{"type":"integer","format":"int64","description":"Request of memory after apply of automation."},"memMiBLimits":{"type":"integer","format":"int64","description":"Limit of memory before apply of automation."},"recommendMemMiBLimits":{"type":"integer","format":"int64","description":"Limit of memory after apply of automation."},"memMiBRequestImpact":{"type":"integer","format":"int64","description":"Impact of automation on memory request value."},"memMiBLimitImpact":{"type":"integer","format":"int64","description":"Impact of automation on memory limit value."},"memRequestChangePercent":{"type":"number","format":"double","description":"Single instance memory request change in %."},"memLimitChangePercent":{"type":"number","format":"double","description":"Single instance memory limit change in %."},"memMiBRequestChangeAbsolute":{"type":"integer","format":"int64","description":"Single instance memory request absolute value change."},"memMiBLimitChangeAbsolute":{"type":"integer","format":"int64","description":"Single instance memory limit absolute value change."}},"required":["memMiBRequest","recommendMemMiBRequest","memMiBLimits","recommendMemMiBLimits","memMiBRequestImpact","memMiBLimitImpact","memRequestChangePercent","memLimitChangePercent","memMiBRequestChangeAbsolute","memMiBLimitChangeAbsolute"]},"Indicator":{"type":"object","description":"A risk/waste signal addressed by an automation change.","properties":{"name":{"type":"string","description":"Indicator name."},"type":{"type":"string","description":"Indicator type (e.g. risk, waste, none)."},"severity":{"type":"integer","description":"Severity level (0 = none, higher = more severe)."}},"required":["name","type","severity"]},"ExecutionType":{"type":"string","enum":["regular-eviction","inplace-resize","cleanup"]},"Pagination":{"type":"object","properties":{"has_next":{"type":"boolean","deprecated":true,"description":"Deprecated: derive from 'next' (true when 'next' is non-null)."},"next":{"type":"string","nullable":true,"description":"An opaque token to be used in the 'page_token' parameter to fetch the next page."},"has_prev":{"type":"boolean","deprecated":true,"description":"Deprecated: derive from 'prev' (true when 'prev' is non-null)."},"prev":{"type":"string","nullable":true,"description":"An opaque token to be used in the 'page_token' parameter to fetch the previous page."},"page_size":{"type":"integer","description":"The maximum number of items to return. Defaults to 1000. Must be between 1 and 5000."}},"required":["has_next","next","has_prev","prev","page_size"]}}},"paths":{"/automation/audit_logs":{"post":{"operationId":"AutomationAuditLogs","summary":"Get Automation Audit Logs with Cursor Pagination","description":"Retrieves a list of automation audit log entries within a specified time range, using cursor-based pagination.\n","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"string","format":"date-time","description":"The start of the time range in ISO 8601 format with UTC timezone (e.g., \"2025-09-17T00:00:00Z\").\nIf omitted, defaults to 00:00:00 UTC of the day that is 30 days ago from today.\n\nConstraints:\n- Must be in UTC timezone (ends with 'Z' or has '+00:00' offset)\n- Cannot be in the future\n- Cannot be before 00:00:00 UTC of 30 days ago from today\n- Must be before 'to' date when both are provided\n- Must be different from 'to' date\n\nExample: If today is 2025-10-17, the earliest allowed value is \"2025-09-17T00:00:00Z\"\n"},"to":{"type":"string","format":"date-time","description":"The end of the time range in ISO 8601 format with UTC timezone (e.g., \"2025-10-17T23:59:59Z\").\nIf omitted, defaults to the current time in UTC.\n\nConstraints:\n- Must be in UTC timezone (ends with 'Z' or has '+00:00' offset)\n- Cannot be in the future\n- Must be after 'from' date when both are provided\n- Must be different from 'from' date\n"},"page_size":{"type":"integer","default":1000,"minimum":1,"maximum":5000,"description":"The maximum number of items to return. Defaults to 1000. Must be between 1 and 5000."},"page_token":{"type":"string","description":"An opaque token from the 'next' or 'prev' field in the response's pagination object, used to fetch the next/previous page. The token encodes both the boundary position and the page direction. Preferred over the deprecated 'after'/'before' fields."},"after":{"type":"string","deprecated":true,"description":"Deprecated: use 'page_token' instead. An opaque token from the 'next' field in the response's pagination object, used to fetch the next page."},"before":{"type":"string","deprecated":true,"description":"Deprecated: use 'page_token' instead. An opaque token from the 'prev' field in the response's pagination object, used to fetch the previous page."},"cluster_uids":{"type":"array","items":{"type":"string"},"description":"Filter by cluster UIDs. Accepts multiple cluster UIDs to filter the audit logs."},"namespaces":{"type":"array","items":{"type":"string"},"description":"Filter by Kubernetes namespaces. Accepts multiple namespaces to filter the audit logs."}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AutomationLogEntry"}},"meta":{"type":"object","properties":{"pagination":{"$ref":"#/components/schemas/Pagination"}}}},"required":["data","meta"]}}}}}}}}}
```

## Deleting an API Token

In certain situations, you may find it necessary to delete an API token.&#x20;

Follow these easy steps:

1. Click on the **`Profile`** button in the bottom left corner and select **`Organization Settings`**.&#x20;
2. In the pop-up window, navigate to the **`API Tokens`** tab.
3. Click the hamburger on the right-hand side, select **`Delete API Token`**, and click the **`Delete`** button.

<figure><img src="/files/rrezl6UUJW48lYx5XxSP" alt=""><figcaption><p>Delete API token</p></figcaption></figure>

## Deleting a cluster

{% hint style="info" %}
The endpoint removes the cluster from PerfectScale without deleting the cluster itself.
{% endhint %}

There may be situations where you need to remove a cluster from PerfectScale. You can seamlessly delete a cluster using Public API:

## DELETE /clusters/{cluster\_uid}

>

```json
{"openapi":"3.0.3","info":{"title":"Perfectscale API - OpenAPI 3.0","version":"v1"},"servers":[{"url":"https://api.app.perfectscale.io/public/v1"}],"security":[{"auth":[]}],"components":{"securitySchemes":{"auth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}}},"paths":{"/clusters/{cluster_uid}":{"delete":{"operationId":"DeleteCluster","parameters":[{"name":"cluster_uid","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"404":{"description":"NotFound"}}}}}}
```

### How to get cluster UID

To get a **cluster UID** with a few clicks, navigate to the **`Overview`** tab, hover over the needed cluster, and click the **`three dots`** button. Then click **`Copy Cluster UID`** so that it will be automatically copied to the clipboard.<br>

<figure><img src="/files/OeoH7jSeQQ90oIf6m9Ex" alt="" width="375"><figcaption><p>Get cluster UID</p></figcaption></figure>

### How to list all cluster UIDs

You can seamlessly list all the clusters' UIDs of the tenant by using the following API:

{% openapi src="/files/EqHf8v7t6x1T7bT9LFNq" path="/clusters" method="get" %}
[openapi-final-2024-07-02.yaml](https://1573387604-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FABMqnYtsOO44JmQTVSnn%2Fuploads%2F6f3eZ5nGXcrp9tOMI7Li%2Fopenapi-final-2024-07-02.yaml?alt=media\&token=f29d2cea-6dd7-44c5-8e02-a8eb794fe493)
{% endopenapi %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.perfectscale.io/api/public-api.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
