Proxmox VEXX · CLI & AutomationPBS automation
PBS REST API: backup automation and integration
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
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.
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'# proxmox-backup-manager user generate-token automation@pbs reportingResult: {
"tokenid": "automation@pbs!reporting",
"value": "REDACTED-SECRET-VALUE"
}Illustrative output
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.
| Operation | Method and path |
|---|---|
| List datastores | GET /admin/datastore |
| Datastore usage and GC status | GET /admin/datastore/{store}/status |
| List snapshots | GET /admin/datastore/{store}/snapshots |
| List backup groups | GET /admin/datastore/{store}/groups |
| Delete one snapshot | DELETE /admin/datastore/{store}/snapshots |
| Start garbage collection | POST /admin/datastore/{store}/gc |
| Read GC status and schedule | GET /admin/datastore/{store}/gc |
| Start verification | POST /admin/datastore/{store}/verify |
| Prune a group by retention | POST /admin/datastore/{store}/prune |
| Usage across all datastores | GET /status/datastore-usage |
| Configured (not runtime) datastores | GET /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.
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}'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# (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 2147483648Illustrative 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:
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.
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"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 .dataPBS=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
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'# (the command above)main 61% 2027-03-14T00:00:00Z
offsite 88% 2026-10-02T00:00:00Z
archive 12% no estimateIllustrative 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.
DatastoreAuditis enough for reporting;DatastoreBackupandDatastoreAdminare not needed to read status, andDatastoreAdmincan 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 PBSAuthorizationheader. PBS wantsPBSAPIToken TOKENID:SECRET, and a malformed header returns the same 401 as a wrong secret. - Omitting the
/adminprefix./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-runfirst. There is no confirmation and no undo. - Reporting backup age without verification state. An unverified
backup is a hypothesis; a
failedone 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-tokenand 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-runa prune and read everyREMOVEline. GET /status/datastore-usagereturnsestimated-full-date, computed by PBS from its own history.- Report on the
verificationstate, 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
Q1. A script that works against the PVE API returns 401 from PBS with a freshly generated token. What should you check first?
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.
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.
Q4. Why does garbage collection not immediately reclaim the space freed by a prune?
Passing score: 75%. Answers are checked in this browser.