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.

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
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

/api/v1/public/metrics answers with a Prometheus text exposition covering nodes, guests and backup freshness. Two ready-made files ship with the image:

FilePurpose
/integrations/prometheus-scrape-config.ymlScrape job to paste into your Prometheus configuration
/integrations/grafana-dashboard-proxcenter.jsonGrafana dashboard to import

The scrape job needs a token holding nodes:read, vms:read and backups:read:

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

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 is additionally reserved for a super_admin acting in the provider tenant.