This document describes the format of the API and can either be read by a human or fed to an AI.

Endpoint: GET /pdm-api
Response content type: application/json
JSON naming convention: lower camel case.

There are three response variants.

HTTP 200 means a complete DashboardSnapshot is available. The snapshot can be live or cached. Check health.status rather than assuming HTTP 200 always means fresh data.

HTTP 401 body:
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Authentication is required."
  }
}

HTTP 503 body, returned when no successful snapshot exists yet:
{
  "error": {
    "code": "DATA_UNAVAILABLE",
    "message": "Infrastructure data is not available yet. Please retry shortly."
  },
  "health": {
    "status": "unavailable",
    "pdmHealthy": true or false,
    "nextPollAt": "ISO-8601 timestamp",
    "polling": true or false
  }
}

The 503 response does not contain metadata, summary, or resource arrays.

All timestamps are ISO 8601/RFC 3339 UTC strings, for example 2026-07-16T12:34:56.789Z.
All durations are non-negative whole seconds.
All capacity and memory values are numeric source values, normally bytes.
All percentages are numbers from 0 through 100, rounded to at most two decimals.
Missing normalized values are omitted. They are not emitted as null, zero, false, empty string, or empty object.
Required arrays are always emitted in a 200 response and can be empty.
Identifiers are opaque strings. Preserve and compare them as strings. Do not parse them as numbers and do not derive identity from display names.

HTTP 200 DashboardSnapshot structure:
{
  "metadata": Metadata,
  "health": SnapshotHealth,
  "summary": Summary,
  "remotes": Remote[],
  "nodes": Node[],
  "virtualMachines": Workload[],
  "containers": Workload[],
  "storageDevices": Storage[],
  "backupServers": BackupServer[]
}

Metadata structure:
{
  "generatedAt": "ISO-8601 timestamp",
  "schemaVersion": "1.0"
}

generatedAt is the time the normalized payload was generated. schemaVersion is currently always the string "1.0".

SnapshotHealth structure in an HTTP 200 response:
{
  "status": "live" or "cached",
  "pdmHealthy": true or false,
  "lastSuccessfulRefreshAt": "ISO-8601 timestamp",
  "nextPollAt": "ISO-8601 timestamp",
  "cacheAgeSeconds": non-negative integer,
  "polling": true or false,
  "lastError": optional string
}

status "live" means the latest refresh succeeded. pdmHealthy is true.
status "cached" means the resource payload is the last successful snapshot after a refresh failure. pdmHealthy is false. The payload can still be displayed, but it is stale.
lastError is optional. If present, it is a sanitized human-readable message. Do not parse it for application logic.
polling means a refresh is in progress. It does not invalidate the supplied snapshot.
The status value "unavailable" is only used in the HTTP 503 response, not a 200 snapshot.

Summary structure:
{
  "nodes": {
    "total": integer,
    "online": integer,
    "offline": integer
  },
  "virtualMachines": {
    "total": integer,
    "running": integer,
    "stopped": integer
  },
  "containers": {
    "total": integer,
    "running": integer,
    "stopped": integer
  },
  "storage": {
    "total": integer,
    "online": integer,
    "capacity": Metric
  },
  "backupServers": {
    "total": integer,
    "online": integer,
    "capacity": Metric
  }
}

All summary counts are non-negative integers.
nodes.total equals nodes.online plus nodes.offline.
virtualMachines.total equals virtualMachines.running plus virtualMachines.stopped.
containers.total equals containers.running plus containers.stopped.
storage.total equals the number of storageDevices items.
storage.online equals the number of storageDevices whose online field is true.
backupServers.total equals the number of backupServers items.
backupServers.online equals the number of backupServers whose online field is true.

A VM or container counts as running only when powerState is "running" or "online", case-insensitive. All other powerState values count as stopped in the summary, including unknown future values.

Metric structure:
{
  "total": optional number,
  "used": optional number,
  "free": optional number,
  "usedPercent": optional number
}

Metric is used for node memory, workload memory, storage capacity, backup server capacity, and aggregate capacity summaries.
total is total capacity or memory.
used is used capacity or memory.
free is present only when total and used are both known. free equals max(0, total minus used).
usedPercent is present only when total is greater than zero and used is known. It equals used divided by total times 100, clamped from 0 through 100 and rounded to two decimals.
Metric members can be absent independently. Never infer missing values as zero.

Remote structure:
{
  "id": string,
  "name": string,
  "type": optional string,
  "online": boolean,
  "metadata": MetadataMap
}

id is the remote identifier. name is the display/source name. Resource remoteName fields refer to the remote name when matching source data is present. Compare remoteName to Remote.name, not necessarily Remote.id.
Known Remote.type values include "pve" and "pbs". Other values or no type are valid.

Node structure:
{
  "id": string,
  "displayName": string,
  "remoteName": optional string,
  "clusterName": optional string,
  "online": boolean,
  "cpuPercent": optional number,
  "memoryPercent": optional number,
  "cpu": optional {
    "cores": optional number,
    "usage": optional number
  },
  "memory": Metric,
  "uptimeSeconds": optional non-negative integer,
  "kernelVersion": optional string,
  "osVersion": optional string,
  "metadata": MetadataMap
}

displayName is for presentation. cpuPercent is normalized CPU usage from 0 through 100. cpu.usage is an unnormalized source value and is commonly a ratio from 0 through 1; do not assume it is already a percentage. cpu.cores is a count when present.
memoryPercent equals memory.usedPercent when both are present.
An offline node can still contain historical metric and uptime fields.

Workload structure, used by both virtualMachines and containers:
{
  "id": string,
  "displayName": string,
  "kind": "virtualMachine" or "container",
  "node": optional string,
  "clusterName": optional string,
  "remoteName": optional string,
  "powerState": string,
  "cpuPercent": optional number,
  "memoryPercent": optional number,
  "memory": Metric,
  "uptimeSeconds": optional non-negative integer,
  "tags": string[],
  "metadata": MetadataMap
}

Every item in virtualMachines has kind "virtualMachine".
Every item in containers has kind "container".
powerState is an open-ended source state string. Known values include "running", "online", and "stopped". Preserve unfamiliar values such as "paused" or "unknown".
node can be absent for incomplete or offline resource data.
tags is always an array and is empty when no tags exist.
memoryPercent equals memory.usedPercent when both are present.
A stopped workload can retain historical CPU, memory, or uptime fields. Determine state from powerState, not the presence or absence of metrics.

Storage structure:
{
  "id": string,
  "name": string,
  "clusterName": optional string,
  "remoteName": optional string,
  "node": optional string,
  "storageType": optional string,
  "online": boolean,
  "capacity": Metric,
  "metadata": MetadataMap
}

storageType is an open-ended source classification. Known values include "pve-storage" and "pbs-datastore".
node can be absent for storage that is not node-scoped.
A PBS datastore can appear in storageDevices and also in BackupServer.datastores. These are duplicates of the same logical datastore. Deduplicate by Storage.id when combining these locations.

BackupServer structure:
{
  "id": string,
  "displayName": string,
  "remoteName": optional string,
  "online": boolean,
  "datastores": Storage[],
  "capacity": Metric,
  "metadata": MetadataMap
}

datastores is always an array. It contains storage records with the same remoteName as the backup server and storageType "pbs-datastore". Those same records also appear in storageDevices.
BackupServer.capacity comes from the backup server record and is not guaranteed to equal the sum of datastore capacities.

MetadataMap structure:
{
  "any string key": any valid JSON value
}

metadata is always present and can be an empty object.
Known optional metadata keys are type, template, ha, pool, vmid, maxcpu, maxmem, level, and lastUpdate.
type is usually the original PDM resource type, for example pve-qemu or pbs-node.
template can be boolean, number, or string.
ha and lastUpdate are source-defined values.
pool is usually a string.
vmid, maxcpu, maxmem, and level can be numbers or strings.
New metadata keys and JSON value types are permitted. Ignore unknown keys safely. Use normalized top-level fields instead of metadata whenever a normalized equivalent exists.

Consumer rules:

First branch on HTTP status. Do not parse a 401 or 503 body as a DashboardSnapshot.
For HTTP 200, require all DashboardSnapshot top-level fields. Permit optional nested fields to be absent.
Treat every array as possibly empty.
Use id for keys, joins, and deduplication. Use displayName or name only for user-facing labels.
Use health.status to decide whether data is stale. cacheAgeSeconds can support additional display policy.
Do not add backupServers.datastores to storageDevices when calculating global capacity because PBS datastore records are duplicated by design.
Do not treat powerState, storageType, Remote.type, or metadata keys as closed enumerations.
