Skip to main content
RunBook Academy

Proxmox VEXX · CLI & AutomationPBS automation

PBS REST API: backup automation and integration

Advanced⏱ ~26 mincurljq

What you'll learn

  • Authenticate to the PBS API with a token, using the header format PBS actually expects
  • Explain why PBS cannot be asked to take a backup, and call the endpoint that can
  • Trigger garbage collection, verification and pruning through the API and follow the task
  • Build backup reporting from the datastore status and usage endpoints
  • Recognise the API path prefix that most PBS scripting examples get wrong

Prerequisites

Verified against Proxmox VE 9.2.4 · Proxmox Backup Server 4.2.5 · Ceph Squid / Tentacle · Debian 13 (Trixie) · Linux kernel 7.0 (PVE 9.2 default) · 2026-08-12

Not yet marked complete on this device.

PBS has its own REST API, on port 8007, in the same style as PVE’s. Use it to integrate backup operations into your own tooling, report on datastore health, and drive maintenance from a scheduler you control.

Two things about it are different enough from PVE to be worth learning before you write anything, and both are places where copied examples routinely fail.

Authentication: the header is not the PVE one

Create the token on the PBS host. The command is generate-token, and the secret it returns is displayed once.

Configuration changecreate a PBS API token
proxmox-backup-manager user create automation@pbs --comment "reporting pipeline"

proxmox-backup-manager user generate-token automation@pbs reporting

proxmox-backup-manager acl update /datastore/main DatastoreAudit \
--auth-id 'automation@pbs!reporting'
Read-only / Safewhat generate-token returns
# proxmox-backup-manager user generate-token automation@pbs reporting
Result: {
"tokenid": "automation@pbs!reporting",
"value": "REDACTED-SECRET-VALUE"
}

Illustrative output

Read-only / Safea first authenticated call
PBS=https://pbs.example.com:8007/api2/json
PBS_TOKEN='automation@pbs!reporting:REPLACE_ME'

curl -sS -H "Authorization: PBSAPIToken $PBS_TOKEN" "$PBS/status" | jq '.data'

The path prefix nearly every example gets wrong

Datastore operations do not live at /api2/json/datastore/{store}. They live under /api2/json/admin/datastore/{store}, and the shorter path — which appears in a great deal of circulating example code — returns a 404 that reads like the datastore does not exist.

OperationMethod and path
List datastoresGET /admin/datastore
Datastore usage and GC statusGET /admin/datastore/{store}/status
List snapshotsGET /admin/datastore/{store}/snapshots
List backup groupsGET /admin/datastore/{store}/groups
Delete one snapshotDELETE /admin/datastore/{store}/snapshots
Start garbage collectionPOST /admin/datastore/{store}/gc
Read GC status and scheduleGET /admin/datastore/{store}/gc
Start verificationPOST /admin/datastore/{store}/verify
Prune a group by retentionPOST /admin/datastore/{store}/prune
Usage across all datastoresGET /status/datastore-usage
Configured (not runtime) datastoresGET /config/datastore

The /config/ versus /admin/ split is the same idea as elsewhere in Proxmox: /config/ is what is defined, /admin/ is what is happening.

Read-only / Safelist datastores and inspect one
PBS=https://pbs.example.com:8007/api2/json
H="Authorization: PBSAPIToken $PBS_TOKEN"

curl -sS -H "$H" "$PBS/admin/datastore" | jq -r '.data[].store'

STORE=main
curl -sS -H "$H" "$PBS/admin/datastore/$STORE/status" | jq '.data | {total, used, avail}'
Read-only / Safewhat is in the datastore, and did it verify?
PBS=https://pbs.example.com:8007/api2/json
H="Authorization: PBSAPIToken $PBS_TOKEN"
STORE=main

curl -sS -H "$H" "$PBS/admin/datastore/$STORE/snapshots" \
| jq -r '.data[] | [(."backup-type"), (."backup-id"), (."backup-time"|todate), (.verification.state // "unverified"), .size] | @tsv' \
| sort -k3
Read-only / Safesnapshots with their verification state
# (the command above)
vm	100	2026-08-11T02:00:07Z	ok	  536870912000
vm	101	2026-08-11T02:14:33Z	ok	   53687091200
vm	141	2026-08-11T02:31:52Z	failed	  214748364800
ct	205	2026-08-11T02:40:11Z	unverified	 2147483648

Illustrative output

PBS cannot be asked to take a backup

This is the second structural difference, and it invalidates a whole category of scripts people try to write.

Backups are pushed to PBS, never pulled by it. The client — either proxmox-backup-client on a standalone host, or PVE’s vzdump for guests — reads the data, chunks it, and uploads it. PBS is a destination. There is no endpoint on PBS that means “go and back up VM 100”, because PBS has no access to VM 100 and no way to quiesce it.

So a request for an on-demand backup goes to PVE, not to PBS:

Service impact possiblerequest a backup — from PVE, targeting the PBS storage
PVE=https://pve-01.example.com:8006/api2/json
NODE=pve-01
VMID=100

UPID=$(curl -sS -X POST \
-H "Authorization: PVEAPIToken $PVE_TOKEN" \
--data-urlencode "vmid=$VMID" \
--data-urlencode "storage=backup-pbs" \
--data-urlencode "mode=snapshot" \
--data-urlencode "bwlimit=102400" \
--data-urlencode "notes-template=pre-migration {{guestname}}" \
"$PVE/nodes/$NODE/vzdump" | jq -r .data)

echo "task: $UPID"

Note that the PVE token in that call uses PVE’s = separator, and the PBS calls above use PBS’s : — a script that talks to both needs two differently-shaped headers, which is worth a comment in the code.

Maintenance operations, and following them

Garbage collection, verification and pruning are all long-running and all return a UPID.

Service impact possiblestart garbage collection and follow it
PBS=https://pbs.example.com:8007/api2/json
H="Authorization: PBSAPIToken $PBS_TOKEN"
STORE=main

curl -sS -H "$H" "$PBS/admin/datastore/$STORE/gc" \
| jq '.data | {"last-run-state", "next-run", "removed-bytes", "pending-bytes"}'

UPID=$(curl -sS -X POST -H "$H" "$PBS/admin/datastore/$STORE/gc" | jq -r .data)
echo "gc task: $UPID"
Service impact possibleverify a datastore, skipping what is already known good
PBS=https://pbs.example.com:8007/api2/json
H="Authorization: PBSAPIToken $PBS_TOKEN"
STORE=main

curl -sS -X POST -H "$H" \
--data-urlencode "ignore-verified=1" \
--data-urlencode "outdated-after=30" \
"$PBS/admin/datastore/$STORE/verify" | jq -r .data
Data-loss riskprune by retention — always dry-run first
PBS=https://pbs.example.com:8007/api2/json
H="Authorization: PBSAPIToken $PBS_TOKEN"
STORE=main

curl -sS -X POST -H "$H" \
--data-urlencode "backup-type=vm" \
--data-urlencode "backup-id=100" \
--data-urlencode "keep-daily=7" \
--data-urlencode "keep-weekly=4" \
--data-urlencode "keep-monthly=6" \
--data-urlencode "dry-run=1" \
"$PBS/admin/datastore/$STORE/prune" \
| jq -r '.data[] | [(."backup-time"|todate), (if .keep then "KEEP" else "REMOVE" end)] | @tsv'

Reporting: PBS already computes the number you want

Read-only / Safeusage across every datastore, with a projected full date
PBS=https://pbs.example.com:8007/api2/json
H="Authorization: PBSAPIToken $PBS_TOKEN"

curl -sS -H "$H" "$PBS/status/datastore-usage" | jq -r '
.data[] | [ .store,
            ((.used / .total * 100) | floor | tostring + "%"),
            (if ."estimated-full-date" then (."estimated-full-date"|todate) else "no estimate" end)
          ] | @tsv'
Read-only / Safethe capacity report, in one call
# (the command above)
main	61%	2027-03-14T00:00:00Z
offsite	88%	2026-10-02T00:00:00Z
archive	12%	no estimate

Illustrative output

For time-series work, GET /status/metrics returns the same underlying data in a form intended for a metrics collector, which is a better base for a Grafana dashboard than polling and reshaping the admin endpoints yourself.

Production considerations

  • Grant the narrowest role that works. DatastoreAudit is enough for reporting; DatastoreBackup and DatastoreAdmin are not needed to read status, and DatastoreAdmin can prune.
  • PBS is a separate trust boundary. A PVE token compromise should not imply a PBS one. Use different identities, and do not let PVE hold a PBS token that can delete backups — that is precisely the credential ransomware looks for.
  • Restrict port 8007. The PBS API listens there; limit it by firewall or network ACL to the hosts that need it.
  • Space out bulk calls. Add a short sleep between iterations when walking many groups; PBS is doing real work behind the status endpoints.
  • Never log the token. Mask it in any debug output, and keep it out of command lines.

Common mistakes

  • Using PVE’s = separator in the PBS Authorization header. PBS wants PBSAPIToken TOKENID:SECRET, and a malformed header returns the same 401 as a wrong secret.
  • Omitting the /admin prefix. /api2/json/datastore/... 404s in a way that looks like a missing datastore.
  • Looking for an endpoint that starts a backup. PBS is a destination; the request goes to PVE’s vzdump.
  • Treating an empty result as “no backups”. A token without ACLs sees an empty list, not a 403.
  • Pruning without dry-run first. There is no confirmation and no undo.
  • Reporting backup age without verification state. An unverified backup is a hypothesis; a failed one is worse than absent.
  • Running GC during the backup window. It is I/O heavy and its safety cutoff interacts with in-flight uploads.
  • Assuming a POST response is a result. GC, verify and prune all return a UPID and run asynchronously.

Key takeaways

  • PBS tokens are created with proxmox-backup-manager user generate-token and the secret is shown once.
  • The header is Authorization: PBSAPIToken TOKENID:SECRET — a colon, not PVE’s equals sign.
  • Datastore operations live under /api2/json/admin/datastore/{store}; /config/ is what is defined, /admin/ is what is happening.
  • PBS never initiates a backup. On-demand backups are requested from PVE via POST /nodes/{node}/vzdump.
  • GC, verify and prune return UPIDs and run asynchronously.
  • Always dry-run a prune and read every REMOVE line.
  • GET /status/datastore-usage returns estimated-full-date, computed by PBS from its own history.
  • Report on the verification state, not just on backup existence.
  • A token with no ACL returns an empty list rather than a 403, so assert on a non-empty expected result.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A script that works against the PVE API returns 401 from PBS with a freshly generated token. What should you check first?

  2. Q2. There is no PBS API endpoint that starts a backup of a VM, because PBS is a destination that receives chunks pushed by a client rather than a system that reads source data.

  3. Q3. A monitoring script calls GET /api2/json/admin/datastore and receives HTTP 200 with an empty data array. Which are plausible explanations worth checking? Select all that apply.

  4. Q4. Why does garbage collection not immediately reclaim the space freed by a prune?

Passing score: 75%. Answers are checked in this browser.