If you've ever clicked through the Proxmox VE web interface to start the same three VMs every morning, or manually spun up a batch of test containers one at a time, you've probably wondered if there's a faster way. There is. Every button in the Proxmox web UI is really just a front end for a REST API, and that same API is sitting there waiting for you to talk to it directly.

This guide walks you through calling that API from Python, using a small library called proxmoxer that takes care of the fiddly HTTP details for you. You don't need to be a professional developer. If you can follow a recipe, you can follow this.

What You Will Learn

  • What the Proxmox VE REST API actually is and how it relates to the web UI and the pvesh command
  • How to create a properly scoped API token for automation, using pveum
  • How to install and use proxmoxer, a Python wrapper for the API
  • How to list nodes, VMs, and containers with a few lines of Python
  • How to start, stop, and create guests programmatically
  • How to deal with self-signed certificate warnings the right way
  • Common errors you'll hit and how to fix each one

What Is This Feature?

The Proxmox VE REST API is the interface layer that the web GUI itself is built on top of. When you click "Start" on a VM in the browser, the GUI sends an HTTPS request to an endpoint like /nodes/pve1/qemu/100/status/start. The API server on port 8006 handles that request, tells the QEMU process to boot, and sends a job ID back so the GUI can show you a progress bar.

Because the API is just HTTPS with JSON responses, anything that can make an HTTPS request can drive Proxmox: curl, a browser, Ansible, Terraform, or a Python script. pvesh, which we covered in an earlier CLI basics guide, is Proxmox's own command-line client for this same API. Python with proxmoxer is a different way to reach the exact same endpoints, but with the ability to loop, branch, and integrate with other tools the way a real program can.

Proxmoxer itself doesn't reinvent anything. It builds the URLs, attaches your authentication header, and turns the JSON responses into Python dictionaries and lists you can work with directly.

Why Would You Use It?

Clicking through the GUI is fine for one-off changes. It stops being fine the moment you're repeating the same steps, or the moment you want something to happen automatically without you sitting at the keyboard.

A few situations where scripting the API earns its keep:

  • Bulk operations. Starting or shutting down twenty test VMs before and after a lab session takes one API call in a loop instead of twenty clicks.
  • Scheduled tasks. A cron job that spins up a fresh container every night for CI testing, then tears it down in the morning.
  • Custom dashboards. Pulling live CPU, memory, and status data into your own internal tool or Homepage-style dashboard instead of relying only on the built-in graphs.
  • Gluing tools together. If you already use Ansible or Terraform to manage infrastructure, this is the same API those tools call under the hood — understanding it makes their behavior a lot less mysterious.

Honestly, if you only manage two or three VMs in a homelab, you may never need this. But the moment you're managing a dozen guests or repeating the same click-sequence weekly, a fifteen-line script pays for itself fast.

Prerequisites

Before you start, make sure you have:

  • A working Proxmox VE 8.x or 9.x host, reachable over the network on port 8006
  • Root or admin access to that host, at least for the initial token setup
  • A separate machine (your laptop, a management VM, whatever) with Python 3.9 or newer installed
  • Basic comfort with a terminal — you don't need to know Python already, we'll explain each line

You do not need to disable the Proxmox firewall or open any extra ports. Port 8006, which the web UI already uses, is the same port the API listens on.

Step-by-Step Tutorial

1. Create a dedicated API token

Don't reuse your root password for scripts. Create a token instead, scoped to only what the script needs. SSH into your Proxmox host and run:

pveum user add automation@pve --comment "API automation user"
pveum role add Automator -privs "VM.Audit,VM.PowerMgmt,VM.Allocate,VM.Config.Disk,Datastore.AllocateSpace"
pveum aclmod / -user automation@pve -role Automator
pveum user token add automation@pve script-token --privsep 0

Copy the token secret shown in the output — it's only displayed once. Your full token identifier will look like automation@pve!script-token, and the secret is a separate UUID-style string you'll pair with it.

The --privsep 0 flag means the token inherits the full permissions of the user rather than needing its own separate ACL entry. That's fine for a first script. For anything you'll run unattended long-term, drop --privsep 0 and grant the token its own narrower permissions instead.

2. Set up a Python environment

On your management machine, create an isolated environment so this project's packages don't collide with anything else on your system:

python3 -m venv proxmox-automation
source proxmox-automation/bin/activate
pip install proxmoxer requests

proxmoxer is the Proxmox API wrapper. It depends on requests to actually send the HTTP traffic, which is why we're installing both.

3. Write a script that lists your nodes

Create a file called list_nodes.py:

from proxmoxer import ProxmoxAPI

proxmox = ProxmoxAPI(
    "192.168.1.50",
    user="automation@pve",
    token_name="script-token",
    token_value="YOUR-TOKEN-SECRET-HERE",
    verify_ssl=False,
)

for node in proxmox.nodes.get():
    print(node["node"], "-", node["status"])

Replace the IP address with your Proxmox host's address, and the token values with your own. Run it with python3 list_nodes.py. On a single-node setup you'll see one line, something like pve1 - online.

That verify_ssl=False is doing something worth understanding, not just copying. Proxmox ships with a self-signed certificate by default, so your script has no way to confirm it's really talking to your server and not something impersonating it. That's an acceptable trade-off on a trusted home network. It is not something you want in a script that touches the internet — more on that in Best Practices.

4. List every VM and container across your cluster

for node in proxmox.nodes.get():
    node_name = node["node"]
    for vm in proxmox.nodes(node_name).qemu.get():
        print(f"[VM]  {vm['vmid']} {vm['name']} - {vm['status']}")
    for ct in proxmox.nodes(node_name).lxc.get():
        print(f"[LXC] {ct['vmid']} {ct['name']} - {ct['status']}")

Notice the pattern: proxmoxer turns URL path segments into chained Python calls. /nodes/pve1/qemu becomes proxmox.nodes("pve1").qemu.get(). Once that clicks, you can translate almost any endpoint in the official API viewer straight into working code.

5. Start and stop a VM

vmid = 100
node_name = "pve1"

proxmox.nodes(node_name).qemu(vmid).status.start.post()

And to shut it down cleanly, which sends an ACPI shutdown signal rather than pulling the power:

proxmox.nodes(node_name).qemu(vmid).status.shutdown.post()

Both calls return immediately with a task ID, because Proxmox handles these operations asynchronously in the background — the API doesn't wait around for the VM to actually finish booting or stopping before responding.

6. Create a lightweight LXC container from a template

This is where scripting starts to save real time. First, make sure you have a template downloaded (skip this if you already have one):

pveam update
pveam download local debian-13-standard_13.1-1_amd64.tar.zst

Then, from Python:

proxmox.nodes(node_name).lxc.post(
    vmid=9000,
    hostname="test-container",
    ostemplate="local:vztmpl/debian-13-standard_13.1-1_amd64.tar.zst",
    storage="local-lvm",
    rootfs="local-lvm:8",
    memory=512,
    cores=1,
    net0="name=eth0,bridge=vmbr0,ip=dhcp",
    password="ChangeThisPassword123!",
    unprivileged=1,
)

Swap the template filename for whatever you actually downloaded — template names change with each Debian point release, so check pveam available if the one above isn't there anymore. This one call replaces the entire "Create CT" wizard you'd otherwise click through by hand.

Commands Explained

Command or ParameterWhat It Does
pveum user addCreates a new Proxmox user account, separate from any Linux system account
pveum role addDefines a custom role with a specific, limited set of privileges
pveum aclmodAttaches a role to a user (or token) at a given path in the resource tree
pveum user token addGenerates an API token tied to a user, usable in place of a password
verify_ssl=FalseTells proxmoxer to accept the server's self-signed certificate without validation
.get()Sends an HTTP GET request — read-only, used for listing or checking status
.post()Sends an HTTP POST request — used to create resources or trigger actions like start/stop
unprivileged=1Creates the container as unprivileged, which is the safer default for most workloads

Common Errors

401 authentication failure. Your token name, user, or secret doesn't match what's stored on the server. Double-check for a stray space or a token that was regenerated after you copied it down — regenerating invalidates the old secret immediately.

403 permission check failed. The token authenticated fine, but the role attached to it doesn't include the privilege the call needs. Listing VMs needs VM.Audit; starting one needs VM.PowerMgmt; creating one needs VM.Allocate plus Datastore.AllocateSpace on the storage you're writing to.

SSLError / CERTIFICATE_VERIFY_FAILED. You left verify_ssl at its default (True) against a self-signed cert. Either set verify_ssl=False for a trusted internal network, or install a proper certificate on Proxmox and point verify_ssl at your CA bundle instead.

ModuleNotFoundError: No module named 'proxmoxer'. Your virtual environment isn't active, or the install happened in a different environment than the one you're running the script from. Run source proxmox-automation/bin/activate again before executing the script.

ConnectionError / timeout. Usually a wrong IP, a firewall blocking port 8006, or the Proxmox host being on a different VLAN than your script's machine. A quick curl -k https://192.168.1.50:8006/api2/json/version from the same machine will tell you fast whether it's a network problem or a code problem.

Troubleshooting

When something isn't working, don't debug Python first — debug the API call itself. Reproduce the exact same request with curl:

curl -k -H "Authorization: PVEAPIToken=automation@pve!script-token=YOUR-SECRET" \
  https://192.168.1.50:8006/api2/json/nodes

If that curl command fails, the problem is on the Proxmox side — the token, the role, or the network path — and no amount of Python debugging will fix it. If curl succeeds but your script still fails, the problem is in how you're calling proxmoxer, which narrows things down considerably.

A couple of things that trip people up specifically:

  • The = between the token ID and the secret in the Authorization header is easy to mistype as a colon or to drop entirely.
  • Clock drift between your client machine and the Proxmox host can cause odd authentication failures. If you've recently set up the host, check timedatectl on both sides.
  • A token created with --privsep 0 inherits the user's permissions, but a token created without that flag starts with zero permissions of its own until you run pveum aclmod against the token specifically, not just the user.

Best Practices

Give every script or tool its own token. If a laptop with a hardcoded token gets stolen, you want to revoke that one token — not reset credentials for everything that automates your cluster.

Scope roles tightly. A script that only needs to read status data has no business holding VM.Allocate. Fewer privileges means a smaller blast radius if a secret leaks.

Don't hardcode secrets in a script you're going to commit to git. Pull the token value from an environment variable instead:

import os
token_value = os.environ["PVE_TOKEN_SECRET"]

Turn verify_ssl back on for anything reaching across the internet or a network you don't fully trust. On a private homelab VLAN, skipping it is a reasonable shortcut. On anything internet-facing, it isn't.

Set token expiration dates for anything you won't be actively maintaining. A forgotten token with no expiry is exactly the kind of thing that shows up in a security audit a year later with nobody remembering what it was for.

Frequently Asked Questions

Do I need Ansible or Terraform if I'm already scripting the API in Python?

Not necessarily. Those tools add state tracking and idempotency on top of the same API. For quick scripts and one-off automation, plain Python is often simpler and easier to debug.

Can I use this same token with pvesh and curl too?

Yes. A token isn't tied to any particular client — pvesh, curl, proxmoxer, and even the web UI's underlying requests all authenticate against the same token the same way.

Will API automation work against a Proxmox cluster, not just a single node?

Yes. Any node in the cluster can answer API requests for the whole cluster's resources, since cluster nodes share configuration state through Corosync.

Is proxmoxer the only Python option?

No, you could call the API with plain requests and skip proxmoxer entirely. Proxmoxer just saves you from manually building URL strings and headers for every single call.

What happens if two scripts try to start the same VM at once?

Proxmox queues the second request as its own task; it won't corrupt anything, but you may get a "VM is locked" style error if the first task hasn't finished. Check task status before firing off follow-up actions in tight loops.

Conclusion

Once you've made your first successful call, the rest is mostly repetition — pick an endpoint from the API viewer, translate it into a chained proxmoxer call, and run it. Start small. A script that just lists your VMs is a perfectly good place to begin, and it already saves you a login and a few clicks every time you run it. From there, batch start/stop scripts and templated container creation are a short step away, and they're usually the ones that end up saving the most time week to week.