Skip to main content

API Tokens

ProxCenter issues read-only API tokens so external tooling can read your fleet without borrowing a person's account. A token is a service account: it carries its own scopes, its own perimeter of connections, its own quota, and it is deleted on its own without touching anybody's sign-in.

Tokens were introduced in v1.4.7 for dashboards, Prometheus-style scrapers, CI checks and any monitoring tool that only needs to look.

info

These are ProxCenter's own tokens. The Proxmox API tokens used to declare a PVE or PBS connection are a different thing, described in Connect Your Infrastructure.

Enterprise Feature

Read-only API access is unlocked by the ProxCenter API Access add-on license on top of an Enterprise edition. Without it, creating a token is refused and every token call answers HTTP 403 Feature not licensed. If the option lapses, existing tokens stop working but can still be listed and deleted, so you can clean up after the add-on rather than being locked out of your own token table.

Read-only by construction​

Three independent rules make a write impossible rather than merely forbidden:

  • Only GET and HEAD are accepted. Anything else, OPTIONS included, answers HTTP 405 API tokens are read-only with an Allow: GET, HEAD header, decided before any scope or database lookup.
  • Only the endpoints listed under Endpoints are reachable. Everywhere else a token is not an authenticated caller at all and receives the ordinary HTTP 401.
  • Those endpoints expose no write handler to begin with.

Creating a token​

Tokens live in Settings > API. The tab is provider-only: you must be a super_admin in the provider tenant, and the routes behind it require the admin.apitokens permission.

  1. Open Settings > API and click New token.
  2. Enter a Name, and a Description if you want one.
  3. Choose an Expiration: No expiration, 30 days, 90 days, 1 year, or Custom with a number of days.
  4. Select the Tenant the token reads for.
  5. Under Connections, pick the connections it may read. Left empty it means All connections of the tenant, including connections added later.
  6. Tick at least one entry under Scopes.
  7. Click Create.

ProxCenter then shows Copy your token now with the full secret. Copy it into your secret manager and click Done.

danger

The secret is displayed exactly once. Only a peppered HMAC-SHA-256 of it is stored, next to the first 12 characters kept as a lookup prefix, so no one, administrator or database owner, can recover it afterwards. A lost secret means deleting the token and creating another one.

The listing then shows one row per token with its Prefix, Name, Tenant, Scopes, Expires, Last used and Created by, plus the Delete action. Never under Expires means the token has no expiry date, and Never under Last used means it has not been called yet; once it has, that cell carries the date and the caller's IP address.

Created by​

Created by carries the email address of the account that minted the token, captured at creation and never refreshed afterwards. Freezing it is deliberate: the point of the column is to answer "who issued this thing" months later, including after that person has left and their account has been deleted. A token whose creator predates the column, or whose creator cannot be resolved, shows Unknown.

Because the provenance is frozen on the token, deleting a user never deletes the trail of what they issued. It does, however, leave their tokens working: see Offboarding a user.

Token format​

A secret is pxc_ followed by 32 random bytes in base64url, 47 characters in all. The Prefix column shows pxc_ plus the first 8 characters of the random part, which is the only part of the secret ProxCenter ever stores in clear or displays again.

Authenticating a call​

Send the full secret as a bearer token. That is the only accepted form: there is no API key header, no query parameter, no cookie.

curl -fsS \
-H 'Authorization: Bearer pxc_REPLACE_WITH_YOUR_TOKEN' \
https://proxcenter.example.com/api/v1/public/health

No CORS header is emitted on these responses. The API is meant for server-to-server callers, not for a page running in a browser.

A refused call answers with one of the following:

StatusBodyCause
401Invalid or expired API tokenUnknown, deleted, revoked or expired token. Every case is answered identically on purpose, and a WWW-Authenticate: Bearer realm="proxcenter" header is returned
403Route not available to API tokensThe token does not hold a scope the endpoint requires
403Connection not in token scopeThe connection named in the path is outside the token's perimeter
403API token tenant is disabled or missingThe token's tenant was disabled or deleted
403Feature not licensedThe API Access add-on is not active
405API tokens are read-onlyAny method other than GET or HEAD
429Rate limit exceededThe token's quota for the current minute is used up

Endpoints​

Seven endpoints are exposed, all GET:

EndpointScopeReturns
/api/v1/public/healthany valid tokenApplication status plus per-connection reachability, filtered by tenant
/api/v1/public/metricsnodes:read, vms:read or backups:readPrometheus text exposition of fleet metrics
/api/v1/public/backupsbackups:readFleet-wide backup freshness per guest: latest backup date, age in seconds, datastore, PBS server, size and verification state. Guests with no backup at all are listed with a null age
/api/v1/vmsvms:readAggregated VM and container list with status, usage and config-derived fields
/api/v1/inventorynodes:readMulti-cluster inventory tree: clusters, nodes, guests and PBS servers
/api/v1/storagestorage:readEvery storage of the visible PVE connections, with capacity and usage
/api/v1/pbs/{id}/backupsbackups:readSnapshots of one PBS server, every datastore and namespace, paginated

Query parameters that exist today:

EndpointParameters
/api/v1/vmsconnId to restrict to one connection, include=agent to probe the QEMU guest agent on running VMs (about 7 times slower)
/api/v1/inventoryrefresh=true to force a blocking refresh instead of reading the shared cache
/api/v1/pbs/{id}/backupsdatastore, namespace (empty string for the root), type (vm, ct or host), page, pageSize, search

In /api/v1/pbs/{id}/backups, {id} is a ProxCenter connection id. It is checked against the token's perimeter before the handler runs, so a connection the token may not read never reaches Proxmox.

tip

The health, metrics and inventory endpoints answer from ProxCenter's own caches, so scraping them on a short interval does not multiply calls to your Proxmox clusters. The VM, storage and backup listings do query Proxmox and PBS, as do refresh=true on the inventory and include=agent on the VM list. Scrape those less often.

An OpenAPI 3.1 description of all seven endpoints ships with the image and is served at /openapi/proxcenter-public-api.json.

The API reference page​

Since v1.4.9 the same document is rendered inside the product, under Settings > API reference (/settings/api-reference). Open it with the API reference button of the Settings > API tab, or from the command palette (Ctrl/Cmd + K) with Open the API reference. Each endpoint is listed with its parameters and responses, and a Try it panel sends the request to the instance you are logged in to, authenticated with a token as described above, so a call can be checked against your own fleet before it goes into a scraper or a script.

The page is gated exactly like the token tab: a super_admin acting in the provider tenant. It does not depend on the API Access add-on, so the reference is readable before the option is licensed. It follows the dark or light mode of your session and shows the white-label logo and name when branding defines them. Nothing leaves your browser but the requests you send from Try it: the embedded viewer has no request proxy, no telemetry, no remote fonts, no AI assistant, and it does not remember the token you paste.

Scopes​

A scope is a bundle of the same read permissions RBAC already uses. Endpoints that name several scopes are satisfied by any one of them.

ScopeOpens
vms:readThe VM and container list, and the proxcenter_vm_* metrics
nodes:readThe inventory tree, and the proxcenter_node_* metrics
storage:readThe storage list, and the proxcenter_storage_* metrics
backups:readPBS snapshots, fleet backup freshness, and the proxcenter_backup_* metrics
automation:readNothing yet
alerts:readNothing yet
reports:readNothing yet

The last three are selectable in the creation dialog but no endpoint requires them at this stage, so granting them changes nothing. Grant only what your integration reads.

compliance:read was removed in v1.4.8

It opened no endpoint either, but unlike the other three it mapped to admin.compliance, the single permission that guards both the compliance reads and the compliance mutations. Inert as it stood, it would have become a real escalation the day a compliance route joined the allowlist. It is gone from the creation dialog, and a scope may now only bundle read permissions, an invariant the test suite enforces on every scope.

Nothing to do on your side: no shipped endpoint ever required it. An unknown scope contributes no permission at all, so a token that still carries it is simply evaluated on its other scopes.

On the metrics endpoint a missing scope filters the corresponding metric families out of an otherwise normal 200, rather than failing the scrape.

Tenant and connection perimeter​

A token is bound to one tenant, and reads only what that tenant owns. If the tenant is disabled, the token stops working.

Within the tenant, the Connections field narrows the perimeter further. Left empty the token sees every connection of the tenant, present and future; filled in it sees exactly those connections, and a connection that later moves out of the tenant drops out of the perimeter on its own. Every response is filtered through that intersection, and a perimeter that cannot be resolved is treated as empty rather than as unrestricted.

Quotas​

Each token gets its own quota, 600 requests per minute by default, counted in a fixed window aligned on the clock minute.

Every answer to a token carries the current state of that window:

HeaderMeaning
RateLimit-LimitRequests allowed per minute for this token
RateLimit-RemainingRequests left in the current window
RateLimit-ResetSeconds until the window resets

Beyond the quota the call is refused with HTTP 429 Rate limit exceeded and a Retry-After header, in seconds. The window is consumed before the scope check, so a call refused for a missing scope still counts against it.

info

The quota is set when the token is created and cannot be changed afterwards: the creation dialog has no field for it, and there is no edit route. A different ceiling means creating another token.

Counters are held in the frontend process, so on a control-plane HA stack the effective ceiling is multiplied by the number of active nodes.

Each call also records a Last used timestamp and the caller's IP address, refreshed at most once a minute.

Deleting a token​

Click Delete on the row and confirm. The dialog states the consequence plainly: Delete this token? Integrations using it stop working immediately, and this cannot be undone.

Deletion takes effect on the very next call, because every call resolves the token with a fresh indexed lookup and nothing is cached.

warning

Deleting is immediate and final. Integrations using that token stop working at once, and there is no un-delete: issue a new token and update the integration.

The row is removed from the table. What survives is the audit trail: the apitoken.create entry written when the token was issued and the apitoken.delete entry written in the same transaction as the deletion, both carrying the prefix. The token table is therefore a list of tokens that exist, not a history of tokens that once did.

Up to v1.4.7 this action was called Revoke

It stamped the row as revoked and kept it, which meant the table slowly filled with rows nobody could act on any more. Deleting removes the row instead. A token revoked by the older behaviour is still refused at authentication, still carries a read-only Revoked chip, and now carries a Delete button next to that chip so you can finally clear it out.

An expiry date stops a token at its due time without removing anything, and the caller cannot tell an expired token from a deleted or an unknown one: all three answer the same HTTP 401.

Offboarding a user​

A token authenticates on its own. It does not ride on its creator's session, so disabling or deleting the account that minted it changes nothing about the token: it keeps reading your fleet at the same quota until someone deletes it.

Since v1.4.8, the user dialogs on Security & Access > Users say so instead of leaving you to find out. When you disable or delete an account that minted tokens, the dialog lists them, each with its name, its prefix and when it was last used, under one of two warnings:

  • Disabling: This account created N active API token(s). Disabling the account does not stop them: a token authenticates on its own.
  • Deleting: This account created N active API token(s). They survive the deletion: a token authenticates on its own.

A checkbox, Delete these tokens as well, deletes them in the same gesture. It is unchecked by default, on both dialogs, and it is reset every time the dialog opens: keeping a token alive past its creator is a legitimate choice, so nothing is destroyed unless you ask for it in that exact dialog.

When the box is ticked, the tokens are deleted before the account is written. If the deletion fails, the account stays as it was rather than ending up disabled or deleted with live tokens behind it. The audit log records how many tokens went, and which prefixes, alongside the account change.

Listing the tokens of a user requires admin.users, the permission that already governs the Users page, rather than admin.apitokens: an administrator offboarding somebody needs to see what that person issued without being handed the keys to the whole token table.

tip

The same information is available the other way round from Settings > API, where the Created by column names the account that issued each token.

Prometheus and Grafana​

Requires the API option

/api/v1/public/metrics is part of the read-only public API, an Enterprise option. Without it every call on this path answers 403 Feature not licensed, and a Grafana dashboard built on it stays empty.

One scrape job covers your whole estate. ProxCenter answers with its own aggregated view of every cluster it manages, so there is nothing to install on any Proxmox node, and the response is served from ProxCenter's inventory cache rather than fanning out to the hypervisor on each scrape.

The exposition carries 53 metric families as of v1.4.10, up from the 7 the endpoint published before it. Everything the five dashboards chart is in there, and the full list is in the series reference below.

The five dashboards​

Each answers one question rather than scrolling through everything, and they link to one another from a ProxCenter dashboards menu that carries your time range and filters across.

DashboardAnswers
ProxCenter Fleet OverviewIs every cluster reachable, and has it been? Ceph health, high availability state, and the changes an instantaneous count hides, such as a node that bounced overnight
ProxCenter Node PerformanceHow hard are the hosts working, and what runs out first? Load average, CPU, memory headroom, I/O wait, swap, and the host root filesystem with a seven day projection
ProxCenter Guest WorkloadWhat are the guests doing? Running state over time, the biggest CPU and memory consumers, and disk and network throughput
ProxCenter StorageWhere is the space going? Capacity per storage, with shared storages counted once for the cluster and non-shared ones broken down per node
ProxCenter Backup ComplianceWhich guests are actually protected, how stale their backups are, and whether the Proxmox Backup Server datastores behind them have room left

All five ship with the image and import into Grafana 11.0 or newer, using core panels only, so nothing has to be installed alongside them:

FilePurpose
/integrations/prometheus-scrape-config.ymlScrape job to paste into your Prometheus configuration
/integrations/grafana-dashboard-proxcenter.jsonFleet Overview
/integrations/grafana-dashboard-proxcenter-nodes.jsonNode Performance
/integrations/grafana-dashboard-proxcenter-guests.jsonGuest Workload
/integrations/grafana-dashboard-proxcenter-storage.jsonStorage
/integrations/grafana-dashboard-proxcenter-backups.jsonBackup Compliance

The Backup Compliance dashboard counts guests that have never been backed up. That number cannot be derived from backup age alone, because a guest with no backup has no age to report, so a dashboard built on age silently leaves the least protected guests out of its own compliance figure.

The scrape job​

Seeing the whole exposition takes four scopes: nodes:read, vms:read, storage:read and backups:read. Each governs its own families, and a token short of one still answers 200 without them. storage:read alone does not open the endpoint, so grant it alongside at least one of the other three:

scrape_configs:
- job_name: proxcenter
scheme: https
metrics_path: /api/v1/public/metrics
scrape_interval: 60s
scrape_timeout: 30s
authorization:
type: Bearer
credentials: pxc_REPLACE_WITH_YOUR_TOKEN
static_configs:
- targets:
- proxcenter.example.com
A partial token looks like a broken dashboard

A token missing one of the four scopes still answers 200. The families it may not see are filtered out of the response rather than refused, so the panels that need them render empty with no error anywhere. If a whole row of a dashboard is blank, or the Storage dashboard is empty end to end, check the token's scopes first.

Keep the 60 second interval. The values come from a cache refreshed on its own schedule, so a tighter scrape buys no fresher data.

Series reference​

Every series is a gauge unless marked as a counter, and every family is filtered independently by the calling token's scopes.

Counters need rate()

The four guest byte totals are cumulative since the guest started, which is why their names end in _total. Read them with rate(), never as a value: rate(proxcenter_vm_network_receive_bytes_total[5m]) gives bytes per second. Declared as counters, Prometheus handles the reset when a guest restarts instead of reading it as an enormous negative rate.

Build (no scope, any valid token)​

SeriesLabelsMeaning
proxcenter_build_infoversionProxCenter build information

Clusters (scope nodes:read)​

SeriesLabelsMeaning
proxcenter_cluster_upconnection, typeCluster or standalone node reachability (1 online, 0 otherwise)
proxcenter_cluster_degradedconnectionCluster in a degraded state (1 degraded, 0 otherwise)
proxcenter_cluster_ceph_healthconnection, healthCeph health as a state set; health is one of ok, warn, err, unknown. Clusters without Ceph emit nothing

Nodes (scope nodes:read)​

SeriesLabelsMeaning
proxcenter_node_onlineconnection, nodeNode online state (1 online, 0 otherwise)
proxcenter_node_cpu_usage_ratioconnection, nodeNode CPU usage ratio (0 to 1)
proxcenter_node_mem_usage_ratioconnection, nodeNode memory usage ratio (0 to 1)
proxcenter_node_mem_bytesconnection, nodeNode memory in use, in bytes
proxcenter_node_mem_total_bytesconnection, nodeNode memory capacity, in bytes
proxcenter_node_rootfs_usage_ratioconnection, nodeNode root filesystem usage ratio (0 to 1). This is the host root filesystem, NOT cluster storage capacity
proxcenter_node_uptime_secondsconnection, nodeNode uptime in seconds
proxcenter_node_maintenanceconnection, nodeNode in maintenance mode (1 in maintenance, 0 otherwise)
proxcenter_node_load1connection, nodeNode load average over one minute
proxcenter_node_load5connection, nodeNode load average over five minutes
proxcenter_node_load15connection, nodeNode load average over fifteen minutes
proxcenter_node_iowait_ratioconnection, nodeShare of node CPU time spent waiting on I/O (0 to 1)
proxcenter_node_swap_bytesconnection, nodeNode swap in use, in bytes
proxcenter_node_swap_total_bytesconnection, nodeNode swap capacity, in bytes; 0 when the node has no swap
proxcenter_node_rootfs_bytesconnection, nodeNode root filesystem in use, in bytes
proxcenter_node_rootfs_total_bytesconnection, nodeNode root filesystem capacity, in bytes
proxcenter_node_cpu_coresconnection, nodeCPU cores the node reports
proxcenter_node_infoconnection, node, pve_version, kernelNode build information: Proxmox VE version and running kernel

Guests (scope vms:read)​

SeriesLabelsMeaning
proxcenter_vm_statusconnection, node, vmid, name, typeGuest running state (1 running, 0 otherwise)
proxcenter_vm_cpu_usage_ratioconnection, node, vmid, name, typeGuest CPU usage ratio (0 to 1)
proxcenter_vm_agent_enabledconnection, node, vmid, nameGuest agent config flag (1 enabled, 0 otherwise)
proxcenter_vm_mem_usage_ratioconnection, node, vmid, name, typeGuest memory usage ratio (0 to 1)
proxcenter_vm_mem_bytesconnection, node, vmid, name, typeGuest memory in use, in bytes
proxcenter_vm_mem_total_bytesconnection, node, vmid, name, typeGuest memory allocation, in bytes
proxcenter_vm_uptime_secondsconnection, node, vmid, name, typeGuest uptime in seconds
proxcenter_vm_ha_stateconnection, node, vmid, name, type, stateGuest HA state as a state set; guests not managed by HA emit nothing
proxcenter_vm_cpu_coresconnection, node, vmid, name, typeVirtual CPU cores allocated to the guest
proxcenter_vm_mem_host_bytesconnection, node, vmid, name, typeGuest memory as the PVE 9 host accounts it, distinct from the guest-side figure; 0 for a container, which reports none
proxcenter_vm_network_receive_bytes_total (counter)connection, node, vmid, name, typeBytes received by the guest since it started
proxcenter_vm_network_transmit_bytes_total (counter)connection, node, vmid, name, typeBytes sent by the guest since it started
proxcenter_vm_disk_read_bytes_total (counter)connection, node, vmid, name, typeBytes read from the guest's disks since it started
proxcenter_vm_disk_written_bytes_total (counter)connection, node, vmid, name, typeBytes written to the guest's disks since it started

Storage (scope storage:read)​

SeriesLabelsMeaning
proxcenter_storage_total_bytesconnection, storage, type, sharedStorage capacity in bytes. A shared storage is reported ONCE for the cluster, never once per node
proxcenter_storage_used_bytesconnection, storage, type, sharedStorage space in use, in bytes. A shared storage is reported ONCE for the cluster, never once per node
proxcenter_storage_usage_ratioconnection, storage, type, sharedStorage usage ratio (0 to 1), served pre-computed so a consumer never divides by a zero capacity
proxcenter_storage_enabledconnection, storageStorage enabled state (1 enabled, 0 disabled); a disabled storage still reports its capacity
proxcenter_storage_node_total_bytesconnection, storage, nodeCapacity of a non-shared storage on one node. Shared storages emit nothing here, since their capacity is not per node
proxcenter_storage_node_used_bytesconnection, storage, nodeSpace in use of a non-shared storage on one node
proxcenter_storage_node_usage_ratioconnection, storage, nodeUsage ratio (0 to 1) of a non-shared storage on one node, served pre-computed

Backups (scope backups:read)​

SeriesLabelsMeaning
proxcenter_backup_age_secondsconnection, node, vmid, name, type, datastoreSeconds since the most recent backup of this guest
proxcenter_backup_protectedconnection, node, vmid, name, typeGuest backup coverage (1 has at least one backup, 0 has none)
proxcenter_backup_age_seconds gained labels in v1.4.10

It used to carry connection, vmid and datastore only. node, name and type were added so a stale backup can be read without a join, and in Prometheus a label is part of the series identity: a query written against the old three-label series now fans out, and a guest that migrates to another node starts a new series while the old one goes stale.

Aggregate over the guest explicitly rather than over the bare metric:

max by (vmid) (proxcenter_backup_age_seconds)

max by (connection, vmid) if several connections may carry the same VMID. The same correction applies to any recording rule or alerting rule keyed on the old identity. Nothing else in the exposition changed identity.

Proxmox Backup Server (scope backups:read)​

SeriesLabelsMeaning
proxcenter_pbs_upconnectionPBS server reachability (1 online, 0 otherwise)
proxcenter_pbs_infoconnection, versionPBS server build information
proxcenter_pbs_datastore_total_bytesconnection, datastoreDatastore capacity in bytes; 0 for a backend that does not report one, such as S3
proxcenter_pbs_datastore_used_bytesconnection, datastoreDatastore space in use, in bytes
proxcenter_pbs_datastore_available_bytesconnection, datastoreDatastore space available, in bytes
proxcenter_pbs_datastore_usage_ratioconnection, datastoreDatastore usage ratio (0 to 1), served pre-computed so a consumer never divides by a zero capacity
proxcenter_pbs_datastore_snapshotsconnection, datastoreSnapshots held in this datastore
proxcenter_pbs_datastore_guestsconnection, datastore, kindDistinct backup sources in this datastore, by kind (vm, ct, host)

Three families are absent rather than wrong, and every case is deliberate:

  • proxcenter_vm_agent_enabled is missing on most installs. ProxCenter builds its inventory from the Proxmox cluster/resources endpoint, which carries no guest agent configuration flag. The flag is therefore unknown, and an unknown is omitted rather than published as a misleading 0 that would report a working agent as absent.
  • proxcenter_vm_ha_state covers only the HA estate. A guest that HA does not manage emits nothing at all, so counting this family gives you the size of the HA estate, not of the fleet.
  • proxcenter_backup_age_seconds skips a guest that has never been backed up, because it has no age to report. That is exactly why proxcenter_backup_protected exists: it publishes a 0 for those guests so they can be counted.

Three values are surprising but correct:

  • A PBS datastore capacity of 0 means the storage backend does not report one, which is normal for a datastore backed by S3. The space in use is still real, which is why proxcenter_pbs_datastore_usage_ratio is served pre-computed: a dashboard dividing used by capacity would divide by zero.
  • A shared storage emits no per-node series. A Ceph pool or an NFS export has one capacity for the whole cluster, and Proxmox reports it once per node. Summing the raw figures would triple an RBD pool on a three node cluster, so proxcenter_storage_* carries shared storages once and only breaks non-shared ones down per node.
  • proxcenter_vm_mem_host_bytes reads 0 for a container. It is the PVE 9 host-side accounting of a guest's memory, which containers do not report.

Audit trail​

Token management and refusals are written to the audit log under category api_tokens:

ActionNotes
apitoken.createWritten in the same transaction as the token itself, with the tenant, the scopes, the connections and the expiry date
apitoken.deleteWritten in the same transaction as the deletion, once per token, with the prefix. This is the only trace left once the row is gone
apitoken.deniedEvery refused call, with the reason, the HTTP status and the token prefix. Never the secret

Tokens deleted while offboarding a user are recorded on the user's own audit entry, with the number of tokens and their prefixes.

note

Entries written before v1.4.8 use the action apitoken.revoke. Nothing emits it any more, but the historical rows are kept and still read normally.

Successful calls are not audited one by one, the Last used timestamp carries that. Rows written while a token is the caller are attributed to the token, not to a user.

Permissions​

PermissionDescription
admin.apitokensCreate, list and delete API tokens

The Settings > API tab and the Settings > API reference page are additionally reserved for a super_admin acting in the provider tenant.