openapi: 3.1.0
info:
  title: NexoVirt REST API
  version: v1
  description: |
    A clean REST API over Proxmox VE, exposed by the NexoVirt panel at `/api/v1`.

    Use it to discover placement (nodes, storages, networks), discover provisioning
    inputs (plans, OS templates, CT templates, IP zones, SSH keys), create QEMU VMs and
    LXC containers, run lifecycle actions (power, reinstall, password reset, IP config,
    delete), and poll asynchronous jobs and live PVE tasks to completion, all without
    touching the web UI.

    ## Response envelope

    Every response is JSON shaped as `{ success, data, error }`. On success, `success` is
    `true`, `data` holds the payload, and `error` is `null`. On failure, `success` is
    `false`, `data` is `null`, and `error` is a human-readable message, alongside a
    matching HTTP status code (`400`, `401`, `403`, `404`, `409`, `422`, `502`).

    List endpoints that paginate add a `meta` object: `{ page, perPage, total }`. Request
    a page with `?page=<n>&perPage=<1..200>` (default `perPage` is 50, clamped to 200).

    ## Authentication

    Every request needs `Authorization: Bearer <token>`. Create tokens in the panel under
    Settings, API tokens. A token may be host-scoped (restricted to certain host ids),
    IP-allowlisted, and rate-limited. A scoped token gets `403 Forbidden` on any
    `/hosts/{id}/...` path it is not permitted to touch.

    ## Content type

    Send `Content-Type: application/json` for POST bodies. Form-encoded bodies also work.

    ## Async guest creation

    `POST /hosts/{id}/guests` enqueues a provisioning job and returns immediately with
    `{ queued: true, job_id, vmid }`. Poll `GET /hosts/{id}/jobs/{jobId}` until
    `data.status` reaches a terminal state, either `done` or `failed` (transitions through
    `queued` then `running`). On failure, `data.error` carries the reason. The same
    executor that backs the web wizard runs the actual PVE work.
  contact:
    name: NexoVirt
    email: hello@nexovirt.com
  license:
    name: Proprietary
    identifier: LicenseRef-NexoVirt-Proprietary
servers:
  - url: https://{host}/api/v1
    description: Your NexoVirt panel (admin token)
    variables:
      host:
        default: your-panel-host
        description: The hostname (and optional port) of your NexoVirt panel installation.
  - url: https://{host}/api/reseller
    description: Your NexoVirt panel, reseller-scoped API (reseller token)
    variables:
      host:
        default: your-panel-host
        description: The hostname (and optional port) of your NexoVirt panel installation.
security:
  - bearerAuth: []
tags:
  - name: Hosts & nodes
    description: Configured Proxmox VE hosts and their live nodes.
  - name: Inventory
    description: DB-backed storage and network inventory, fast reads with no live PVE call.
  - name: Provisioning discovery
    description: Everything a caller needs to learn before calling create, vmids, plans, templates, IP zones, SSH keys.
  - name: Guests
    description: List, inspect, create, and operate QEMU VMs and LXC containers.
  - name: Jobs & tasks
    description: Poll asynchronous provisioning jobs and live PVE tasks to completion.
  - name: Users
    description: Customer accounts, guest ownership, SSH keys and customer IP pools (admin token required).
  - name: Firewall
    description: Per-guest firewall rules and options, reconciled to PVE.
  - name: Networking
    description: Per-host NAT and port-forwarding.
  - name: Alerts
    description: Alert instances and alert rules.
  - name: Resellers (admin)
    description: >
      Admin management of resellers, their assigned nodes/IP zones, and reseller quota
      plans. Requires an admin `/api/v1` bearer token. Enterprise-only, `403` otherwise.
  - name: 'Reseller: Customers'
    description: >
      A reseller's own customer accounts and their SSH keys, via the separate
      `/api/reseller` server and a reseller bearer token. Every endpoint is scoped to the
      calling reseller's own subtree; a cross-tenant id is a `404`, not a `403`.
  - name: 'Reseller: Guests'
    description: Guest lifecycle (create, inspect, power, delete) over a reseller's own subtree.
  - name: 'Reseller: SSH keys'
    description: A reseller customer's SSH public keys (id/name/fingerprint only, never the key body).
  - name: 'Reseller: Networking'
    description: Static IP changes for a reseller's own guests, within zones assigned to them.
  - name: 'Reseller: Plans & usage'
    description: Read-only plan availability and per-customer usage for the calling reseller.
paths:
  /hosts:
    get:
      operationId: listHosts
      tags: [Hosts & nodes]
      summary: List configured hosts
      description: >
        Returns the panel's configured PVE hosts, scoped to the calling token's host_scope
        (an unscoped token sees all hosts). Never includes credentials.
      responses:
        '200':
          description: Configured hosts.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Host'
              example:
                success: true
                error: null
                data:
                  - id: 1
                    name: Test
                    hostname: 188.40.137.121
                    port: 8006
                    user: root@pam
                    active: true
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      operationId: createHost
      tags: [Hosts & nodes]
      summary: Create a host
      description: Creates an encrypted host row; SSH agent onboarding stays a panel action. Never returns secrets.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, hostname, password]
              properties:
                name: { type: string }
                hostname: { type: string }
                port: { type: integer, default: 8006 }
                username: { type: string, default: root }
                realm: { type: string, default: pam }
                password: { type: string }
                verify_tls: { type: boolean }
      responses:
        '200': { description: Created., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /hosts/{hostId}/nodes:
    get:
      operationId: listNodes
      tags: [Hosts & nodes]
      summary: List a host's live nodes
      description: Live PVE node list for the given host (real-time call to Proxmox).
      parameters:
        - $ref: '#/components/parameters/HostId'
      responses:
        '200':
          description: Live node list.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/NodeSummary'
              example:
                success: true
                error: null
                data:
                  - node: pve1
                    status: online
                    cpu: 0.03
                    maxcpu: 8
                    mem: 4294967296
                    maxmem: 34359738368
                    disk: 21474836480
                    maxdisk: 107374182400
                    uptime: 1234567
                    level: ''
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '502': { $ref: '#/components/responses/BadGateway' }

  /hosts/{hostId}/nodes/{node}:
    get:
      operationId: getNode
      tags: [Hosts & nodes]
      summary: Get live node detail
      description: >
        Live PVE node status for a single node (CPU info, memory, root filesystem, load
        average, PVE version, and so on). The response passes through Proxmox's own node
        status object.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/NodeName'
      responses:
        '200':
          description: Live node status.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/NodeDetail'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { $ref: '#/components/responses/BadGateway' }

  /hosts/{hostId}/nodes/{node}/usage:
    get:
      operationId: getNodeUsage
      tags: [Hosts & nodes]
      summary: Node CPU/mem/disk/net time series
      description: >
        Chart-ready time series built from the DB-backed node metrics table, not a live PVE
        call. Points are `[epoch_ms, value]` pairs.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/NodeName'
        - $ref: '#/components/parameters/Range'
        - $ref: '#/components/parameters/Metrics'
      responses:
        '200':
          description: Node usage series.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/UsageSeries'
              example:
                success: true
                error: null
                data:
                  range: 30m
                  series:
                    - name: cpu
                      unit: '%'
                      points: [[1752480000000, 3.2], [1752480030000, 4.1]]
                    - name: mem
                      unit: '%'
                      points: [[1752480000000, 41.7], [1752480030000, 42.0]]
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /hosts/{hostId}/usage/summary:
    get:
      operationId: getUsageSummary
      tags: [Hosts & nodes]
      summary: Host usage summary
      description: >
        Guests up/down/total and summed cores/memory/disk across the host's latest
        inventory snapshot, DB-backed, no live PVE call.
      parameters:
        - $ref: '#/components/parameters/HostId'
      responses:
        '200':
          description: Usage summary.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/UsageSummary'
              example:
                success: true
                error: null
                data:
                  guests: { up: 7, down: 2, total: 9 }
                  cores: 24
                  mem: 68719476736
                  disk: 900000000000
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /hosts/{hostId}/storages:
    get:
      operationId: listStorages
      tags: [Inventory]
      summary: List storages
      description: >
        Node storages from the latest inventory snapshot (DB-backed, no live PVE read).
        With `?node=`, returns that node's storages plus any datacenter-wide/shared entries
        (a row with an empty node, for example shared NFS or PBS storage, matches every
        node). Paginated. Filter by `content` client-side, for example `images` for VM
        disks or `rootdir` for LXC.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/NodeQuery'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Storages.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Storage'
                      meta:
                        $ref: '#/components/schemas/PaginationMeta'
              example:
                success: true
                error: null
                data:
                  - id: local-lvm
                    node: pve1
                    type: lvmthin
                    total: 500000000000
                    used: 120000000000
                    avail: 380000000000
                    used_pct: 24.0
                    content: images,rootdir
                    active: true
                meta: { page: 1, perPage: 50, total: 3 }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /hosts/{hostId}/networks:
    get:
      operationId: listNetworks
      tags: [Inventory]
      summary: List network interfaces
      description: >
        Node network interfaces from the latest inventory snapshot (DB-backed, no live PVE
        read), enriched with bridge classification. `role` is one of `WAN`, `Private`,
        `Internal`, or `Other`. Paginated.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/NodeQuery'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Network interfaces.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/NetworkInterface'
                      meta:
                        $ref: '#/components/schemas/PaginationMeta'
              example:
                success: true
                error: null
                data:
                  - iface: vmbr0
                    node: pve1
                    type: bridge
                    role: WAN
                    cidr: 203.0.113.0/24
                    gateway: 203.0.113.1
                    ports: eno1
                    active: true
                meta: { page: 1, perPage: 50, total: 2 }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /hosts/{hostId}/next-vmid:
    get:
      operationId: getNextVmid
      tags: [Provisioning discovery]
      summary: Get the next free VMID
      description: >
        A free VMID from the live PVE `cluster/nextid` call. Create still requires an
        explicit `vmid` in the body; use this to obtain one.
      parameters:
        - $ref: '#/components/parameters/HostId'
      responses:
        '200':
          description: Next free VMID.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          next_vmid: { type: integer, example: 105 }
              example:
                success: true
                error: null
                data: { next_vmid: 105 }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '502': { $ref: '#/components/responses/BadGateway' }

  /hosts/{hostId}/plans:
    get:
      operationId: listPlans
      tags: [Provisioning discovery]
      summary: List resource plans
      description: >
        Active resource plans usable as `plan_id` at create time. A plan fills and locks
        `cores`, `memory_mb`, and `disk_gb` on the created guest. Paginated.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Resource plans.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Plan'
                      meta:
                        $ref: '#/components/schemas/PaginationMeta'
              example:
                success: true
                error: null
                data:
                  - id: 2
                    name: Standard
                    description: 2 vCPU, 2 GB RAM, 20 GB disk
                    cores: 2
                    memory_mb: 2048
                    disk_gb: 20
                    traffic_limit_gb: 1000
                    runtime_days: 30
                    grace_days: 3
                    snapshot_limit: 3
                meta: { page: 1, perPage: 50, total: 4 }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /hosts/{hostId}/os-templates:
    get:
      operationId: listOsTemplates
      tags: [Provisioning discovery]
      summary: List QEMU OS templates
      description: >
        Cloud-init OS templates for QEMU. `ref` (for example `tpl:7`) is the exact
        `os_template` value to send at create. Never returns SSH keys or other cloud-init
        secrets. Paginated.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: QEMU OS templates.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/OsTemplate'
                      meta:
                        $ref: '#/components/schemas/PaginationMeta'
              example:
                success: true
                error: null
                data:
                  - id: 7
                    ref: 'tpl:7'
                    name: Ubuntu 24.04
                    description: Ubuntu 24.04 LTS cloud image
                    ostype: l26
                    ci_user: ubuntu
                    group_id: 1
                    group_name: General
                    is_shipped: true
                meta: { page: 1, perPage: 50, total: 6 }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /hosts/{hostId}/ct-templates:
    get:
      operationId: listCtTemplates
      tags: [Provisioning discovery]
      summary: List LXC container templates
      description: >
        LXC template options. `managed` is the panel's own CT template library
        (`lxc_template_id` is the create argument, staged on demand). `available` is live
        PVE `vztmpl` volumes on `?node` (only populated when a valid `node` is given;
        `volid` is the create argument). Without `?node`, `available` is an empty array and
        no PVE call is made. Not paginated.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/NodeQuery'
      responses:
        '200':
          description: Managed and available LXC templates.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/CtTemplates'
              example:
                success: true
                error: null
                data:
                  managed:
                    - lxc_template_id: 3
                      name: Debian 12
                      description: Debian 12 bookworm
                      ostype: debian
                      arch: amd64
                      unprivileged: true
                      group_id: 1
                      source: shipped
                  available:
                    - volid: 'local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst'
                      node: pve1
                      format: tar.zst
                      size: 134217728
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /hosts/{hostId}/ip-zones:
    get:
      operationId: listIpZones
      tags: [Provisioning discovery]
      summary: List IP zones
      description: >
        The host's IP zones (IPAM). `ref` (for example `zone:3`) is the exact `ip_source`
        create value; using it allocates a static IP from the zone's pool. Paginated.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: IP zones.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/IpZone'
                      meta:
                        $ref: '#/components/schemas/PaginationMeta'
              example:
                success: true
                error: null
                data:
                  - id: 3
                    ref: 'zone:3'
                    name: Private LAN
                    family: ipv4
                    bridge: vmbr1
                    cidr: 10.10.0.0/24
                    gateway: 10.10.0.1
                    vlan: ''
                    is_active: true
                meta: { page: 1, perPage: 50, total: 1 }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /users/{userId}/ssh-keys:
    get:
      operationId: listUserSshKeys
      tags: [Provisioning discovery]
      summary: List a user's SSH keys
      description: >
        A user's stored SSH keys, exposing only `id`, `name`, and `fingerprint`, never the
        key body. Pass the owner's id as `owner_id` at create to auto-inject all of that
        user's stored keys. Not paginated.
      parameters:
        - $ref: '#/components/parameters/UserId'
      responses:
        '200':
          description: SSH keys.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/SshKey'
              example:
                success: true
                error: null
                data:
                  - id: 5
                    name: laptop
                    fingerprint: 'SHA256:9f0b6c...'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      operationId: addUserSshKey
      tags: [Users]
      summary: Add an SSH key to a user
      description: Validates, fingerprints and stores the key (deduped per user). Returns id/name/fingerprint only, never the key body.
      parameters:
        - $ref: '#/components/parameters/UserId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [public_key]
              properties:
                name: { type: string }
                public_key: { type: string }
      responses:
        '200': { description: Added., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /hosts/{hostId}/guests:
    get:
      operationId: listGuests
      tags: [Guests]
      summary: List guests
      description: >
        Guest list from the latest inventory snapshot (DB-backed, no live PVE call),
        filterable by `node`, `type`, and `status`. Paginated.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/NodeQuery'
        - name: type
          in: query
          description: Filter by guest type.
          schema: { type: string, enum: [qemu, lxc] }
        - name: status
          in: query
          description: Filter by guest status, for example running or stopped.
          schema: { type: string }
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Guests.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/GuestListItem'
                      meta:
                        $ref: '#/components/schemas/PaginationMeta'
              example:
                success: true
                error: null
                data:
                  - host_id: 1
                    host: Test
                    node: pve1
                    vmid: 105
                    name: web-01
                    type: qemu
                    status: running
                    os: l26
                    ip: 10.10.0.5
                    cores: 2
                    mem: 2147483648
                    storage: 21474836480
                    qemu_agent: true
                meta: { page: 1, perPage: 50, total: 9 }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      operationId: createGuest
      tags: [Guests]
      summary: Create a guest (async)
      description: >
        Creates a QEMU VM or an LXC container. Creation is asynchronous: this call
        validates the request, enqueues a provisioning job, and returns immediately with
        `{ queued: true, job_id, vmid }`. Poll `GET /hosts/{hostId}/jobs/{jobId}` until
        `data.status` is `done` or `failed`. The same executor that backs the web wizard
        runs the actual PVE work, so the guest's plan, template, IP source, and disks are
        applied exactly as they would be from the panel.


        A plan (`plan_id`) fills and locks `cores`, `memory_mb`, and `disk_gb`; otherwise
        supply them explicitly (defaults 1 core, 512 MB, 8 GB). `password` is required
        unless at least one SSH key is present (via `ssh_keys[]` or an `owner_id` with
        stored keys). QEMU requires `os_template` as `tpl:<id>` from `GET
        /hosts/{hostId}/os-templates`. LXC requires exactly one template source, either
        `lxc_template_id` (from the `managed` list) or `os_template` as a raw volid (from
        the `available` list). `ip_source` is `dhcp` (default) or `zone:<id>` (from `GET
        /hosts/{hostId}/ip-zones`) to allocate a static IP. `extra_storage[]`,
        `extra_size[]`, and `extra_mount[]` are parallel arrays describing additional
        disks; LXC mounts must be absolute paths and default to `/mnt/disk<N>` when blank.
      parameters:
        - $ref: '#/components/parameters/HostId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GuestCreateRequest'
            examples:
              qemuFromPlan:
                summary: QEMU VM from a plan, DHCP
                value:
                  type: qemu
                  node: pve1
                  vmid: 105
                  name: web-01
                  plan_id: 2
                  storage: local-lvm
                  bridge: vmbr0
                  os_template: 'tpl:7'
                  ip_source: dhcp
                  password: s3cret-pw
              lxcCustomWithZoneIp:
                summary: LXC container with explicit specs and a static IP
                value:
                  type: lxc
                  node: pve1
                  vmid: 106
                  name: db-01
                  cores: 2
                  memory_mb: 2048
                  disk_gb: 20
                  storage: local-lvm
                  bridge: vmbr1
                  lxc_template_id: 3
                  ip_source: 'zone:3'
                  password: s3cret-pw
                  owner_id: 4
                  extra_storage: [local-lvm]
                  extra_size: [50]
                  extra_mount: ['/data']
              lxcDocker:
                summary: LXC container ready for Docker (unprivileged + nesting + keyctl)
                value:
                  type: lxc
                  node: pve1
                  vmid: 107
                  name: docker-01
                  cores: 2
                  memory_mb: 2048
                  disk_gb: 20
                  storage: local-lvm
                  bridge: vmbr0
                  lxc_template_id: 3
                  password: s3cret-pw
                  unprivileged: true
                  nesting: true
                  keyctl: true
      responses:
        '200':
          description: Guest creation queued.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/ProvisioningQueued'
              example:
                success: true
                error: null
                data: { queued: true, job_id: 42, vmid: 105 }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409':
          description: Conflict, for example the selected IP zone's pool is exhausted.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: false, data: null, error: 'IP pool exhausted in the selected zone.' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { $ref: '#/components/responses/BadGateway' }

  /hosts/{hostId}/guests/{type}/{vmid}:
    get:
      operationId: getGuest
      tags: [Guests]
      summary: Get guest detail
      description: Guest detail from the latest inventory snapshot (DB-backed, no live PVE call).
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
      responses:
        '200':
          description: Guest detail.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/GuestDetail'
              example:
                success: true
                error: null
                data:
                  host_id: 1
                  vmid: 105
                  type: qemu
                  node: pve1
                  name: web-01
                  status: running
                  ip: 10.10.0.5
                  cores: 2
                  mem: 2147483648
                  disk: 21474836480
                  ostype: l26
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      operationId: deleteGuest
      tags: [Guests]
      summary: Delete a guest
      description: Deletes the guest. `node` is required as a query parameter.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
        - name: node
          in: query
          required: true
          description: The PVE node the guest currently lives on.
          schema: { type: string }
      responses:
        '200':
          description: Guest deleted.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { $ref: '#/components/responses/BadGateway' }

  /hosts/{hostId}/guests/{type}/{vmid}/usage:
    get:
      operationId: getGuestUsage
      tags: [Guests]
      summary: Guest CPU/mem/disk/net time series
      description: >
        Chart-ready time series built from the DB-backed guest metrics table, not a live
        PVE call. Points are `[epoch_ms, value]` pairs.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
        - name: node
          in: query
          required: true
          description: The PVE node the guest currently lives on.
          schema: { type: string }
        - $ref: '#/components/parameters/Range'
        - $ref: '#/components/parameters/Metrics'
      responses:
        '200':
          description: Guest usage series.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/UsageSeries'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /hosts/{hostId}/guests/{type}/{vmid}/power:
    post:
      operationId: powerGuest
      tags: [Guests]
      summary: Power action
      description: Starts, stops, shuts down, reboots, suspends, or resumes the guest.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [node, action]
              properties:
                node:
                  type: string
                  description: The PVE node the guest currently lives on.
                action:
                  type: string
                  enum: [start, stop, shutdown, reboot, suspend, resume]
            example: { node: pve1, action: start }
      responses:
        '200':
          description: Power action accepted.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /hosts/{hostId}/guests/{type}/{vmid}/reinstall:
    post:
      operationId: reinstallGuest
      tags: [Guests]
      summary: Reinstall a guest (async)
      description: >
        Deletes and recreates the guest, keeping its vmid and specs, with a fresh OS. For
        QEMU, `os_template` must be `tpl:<id>` from `GET /hosts/{hostId}/os-templates`. For
        LXC, `os_template` is a raw `vztmpl` volid. `password` is required unless at least
        one SSH key is present via `ssh_keys[]`. Asynchronous, same job/poll pattern as
        create.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [node, os_template]
              properties:
                node: { type: string, description: The PVE node the guest currently lives on. }
                os_template: { type: string, description: '`tpl:<id>` for QEMU, or a `vztmpl` volid for LXC.' }
                password: { type: string, description: 'New root password, minimum 5 characters.' }
                ssh_keys:
                  type: array
                  items: { type: string }
                  description: Public key bodies, merged with any owner-stored keys.
                owner_id:
                  type: integer
                  description: When set, the owner's stored SSH keys are merged into the reinstall (same as create).
                name: { type: string, description: Optional new guest name. }
            example:
              node: pve1
              os_template: 'tpl:9'
              password: n3w-s3cret
      responses:
        '200':
          description: Reinstall queued.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/ProvisioningQueued'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { $ref: '#/components/responses/BadGateway' }

  /hosts/{hostId}/guests/{type}/{vmid}/password:
    post:
      operationId: setGuestPassword
      tags: [Guests]
      summary: Reset the guest's root password
      description: >
        Sets a new password for the guest's login user, minimum 5 characters. By default
        (async provisioning enabled) the work is queued and the response is `{queued, job_id}` —
        poll GET /hosts/{hostId}/jobs/{jobId} for the outcome. When async provisioning is off
        the change runs synchronously and the response is `{changed: true}`.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [node, password]
              properties:
                node: { type: string, description: The PVE node the guest currently lives on. }
                password: { type: string, minLength: 5 }
                username: { type: string, description: Defaults to root. }
            example: { node: pve1, password: n3w-s3cret, username: root }
      responses:
        '200':
          description: >
            Queued (default) or, when async provisioning is disabled, changed synchronously.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        oneOf:
                          - { $ref: '#/components/schemas/ProvisioningQueued' }
                          - type: object
                            properties: { changed: { type: boolean, example: true } }
              example: { success: true, error: null, data: { queued: true, job_id: 42 } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { $ref: '#/components/responses/BadGateway' }

  /hosts/{hostId}/guests/{type}/{vmid}/ipconfig:
    post:
      operationId: setGuestIpconfig
      tags: [Guests]
      summary: Set the guest's network configuration
      description: >
        Sets a static or DHCP `ipconfig` on the guest's primary NIC. Reboot the guest to
        apply the change.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [node, ipconfig]
              properties:
                node: { type: string, description: The PVE node the guest currently lives on. }
                ipconfig:
                  type: string
                  description: 'For example "ip=dhcp" or "ip=1.2.3.4/24,gw=1.2.3.1".'
                bridge: { type: string, description: Defaults to vmbr0. }
            example: { node: pve1, ipconfig: 'ip=10.10.0.5/24,gw=10.10.0.1', bridge: vmbr1 }
      responses:
        '200':
          description: IP configuration applied (pending reboot).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: true, error: null, data: { note: 'Reboot the guest to apply.' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { $ref: '#/components/responses/BadGateway' }

  /hosts/{hostId}/jobs/{jobId}:
    get:
      operationId: getProvisioningJob
      tags: [Jobs & tasks]
      summary: Get an async provisioning job's status
      description: >
        Poll this to track a create, reinstall, delete, or power job enqueued when
        `provision.async` is enabled. `status` transitions `queued` to `running` to a
        terminal state, either `done` or `failed`. On failure, `error` carries the reason.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - name: jobId
          in: path
          required: true
          schema: { type: integer }
      responses:
        '200':
          description: Job status.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/ProvisioningJob'
              example:
                success: true
                error: null
                data:
                  id: 42
                  kind: create
                  type: qemu
                  vmid: 105
                  node: pve1
                  status: done
                  error: ''
                  attempts: 1
                  created_at: '2026-07-14 10:00:00'
                  started_at: '2026-07-14 10:00:01'
                  finished_at: '2026-07-14 10:00:42'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /hosts/{hostId}/tasks/{node}/{upid}:
    get:
      operationId: getPveTask
      tags: [Jobs & tasks]
      summary: Get a live PVE task's status
      description: >
        A live PVE task status lookup by UPID, for tracking the underlying Proxmox task
        directly (as opposed to the panel's own provisioning job).
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/NodeName'
        - name: upid
          in: path
          required: true
          description: The PVE task UPID, URL-encoded.
          schema: { type: string }
      responses:
        '200':
          description: Task status.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
        '400':
          description: Invalid task id.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { $ref: '#/components/responses/BadGateway' }

  /users:
    get:
      operationId: listUsers
      tags: [Users]
      summary: List customer users
      description: Lists customer accounts (never includes secrets). Requires an admin-capable token.
      responses:
        '200':
          description: Customer users.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/UserSummary'
              example:
                success: true
                error: null
                data:
                  - id: 4
                    username: alice
                    email: alice@example.com
                    external_id: null
                    is_active: 1
                    guests: 2
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      operationId: createUser
      tags: [Users]
      summary: Create a customer user
      description: >
        Creates a customer account with a generated password, returned once in the
        response. Enqueues a welcome email.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, username]
              properties:
                email: { type: string, format: email }
                username: { type: string }
                user_id: { type: string, description: Optional external id to correlate with your own system. }
            example: { email: alice@example.com, username: alice, user_id: 'ext-1001' }
      responses:
        '200':
          description: User created.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        $ref: '#/components/schemas/UserCreated'
              example:
                success: true
                error: null
                data:
                  id: 4
                  username: alice
                  email: alice@example.com
                  external_id: 'ext-1001'
                  password: 'Xk29-generated-pw'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /users/{userId}:
    delete:
      operationId: deleteUser
      tags: [Users]
      summary: Delete a customer user
      description: Deletes the customer account and its guest ownership assignments.
      parameters:
        - $ref: '#/components/parameters/UserId'
      responses:
        '200':
          description: User deleted.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: true, error: null, data: { deleted: 4 } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /users/{userId}/reset-password:
    post:
      operationId: resetUserPassword
      tags: [Users]
      summary: Reset a customer's password
      description: Generates a new password for the customer and enqueues a notification email.
      parameters:
        - $ref: '#/components/parameters/UserId'
      responses:
        '200':
          description: Password reset.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: true, error: null, data: { id: 4, password: 'Yk91-new-pw' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /users/{userId}/guests:
    post:
      operationId: assignUserGuest
      tags: [Users]
      summary: Assign guest ownership
      description: Assigns ownership of a guest to a customer. One owner per guest.
      parameters:
        - $ref: '#/components/parameters/UserId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [host_id, type, vmid]
              properties:
                host_id: { type: integer }
                type: { type: string, enum: [qemu, lxc] }
                vmid: { type: integer }
            example: { host_id: 1, type: qemu, vmid: 105 }
      responses:
        '200':
          description: Guest assigned.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: true, error: null, data: { user_id: 4, host_id: 1, type: qemu, vmid: 105 } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      operationId: unassignUserGuest
      tags: [Users]
      summary: Unassign guest ownership
      description: Removes the current owner from a guest.
      parameters:
        - $ref: '#/components/parameters/UserId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [host_id, type, vmid]
              properties:
                host_id: { type: integer }
                type: { type: string, enum: [qemu, lxc] }
                vmid: { type: integer }
            example: { host_id: 1, type: qemu, vmid: 105 }
      responses:
        '200':
          description: Guest unassigned.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: true, error: null, data: { unassigned: true } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /hosts/{hostId}/guests/{type}/{vmid}/snapshots:
    get:
      operationId: listSnapshots
      tags: [Guests]
      summary: List guest snapshots
      description: Live PVE read of a guest's snapshots, newest first.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
        - $ref: '#/components/parameters/NodeQuery'
      responses:
        '200': { description: Snapshots., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { $ref: '#/components/responses/BadGateway' }
    post:
      operationId: createSnapshot
      tags: [Guests]
      summary: Create a snapshot
      description: Creates a snapshot. QEMU guests may include RAM state via `vmstate`.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [node, name]
              properties:
                node: { type: string }
                name: { type: string, description: 'Snapshot name (letter then letters/digits/underscore).' }
                description: { type: string }
                vmstate: { type: boolean, description: 'QEMU only: also save the running RAM state.' }
      responses:
        '200': { description: 'Snapshot task started (returns a PVE UPID).', content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { $ref: '#/components/responses/BadGateway' }
  /hosts/{hostId}/guests/{type}/{vmid}/snapshots/{name}/rollback:
    post:
      operationId: rollbackSnapshot
      tags: [Guests]
      summary: Roll back to a snapshot
      description: Reverts the guest to a snapshot. Stop the guest first unless the snapshot has a saved RAM state.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
        - { name: name, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [node], properties: { node: { type: string } } }
      responses:
        '200': { description: 'Rollback task started.', content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { $ref: '#/components/responses/BadGateway' }
  /hosts/{hostId}/guests/{type}/{vmid}/snapshots/{name}:
    delete:
      operationId: deleteSnapshot
      tags: [Guests]
      summary: Delete a snapshot
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
        - { name: name, in: path, required: true, schema: { type: string } }
        - $ref: '#/components/parameters/NodeQuery'
      responses:
        '200': { description: 'Delete task started.', content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { $ref: '#/components/responses/BadGateway' }
  /hosts/{hostId}/guests/{type}/{vmid}/resize:
    post:
      operationId: resizeGuest
      tags: [Guests]
      summary: Resize cores, memory, and/or a disk
      description: >
        Change CPU cores and/or memory (MB), and/or grow a disk. Provide any of `cores`,
        `memory_mb`, or `disk` + `size`. Cores/memory apply on the next reboot; disk grow is
        immediate and grow-only (shrinking is rejected).
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [node]
              properties:
                node: { type: string }
                cores: { type: integer }
                memory_mb: { type: integer }
                disk: { type: string, description: 'Disk slot, for example scsi0 or rootfs.' }
                size: { type: string, description: 'PVE size token, for example +10G.' }
      responses:
        '200': { description: Resized., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { $ref: '#/components/responses/BadGateway' }
  /hosts/{hostId}/guests/{type}/{vmid}/ipfilter:
    get:
      operationId: getGuestIpFilter
      tags: [Guests]
      summary: Anti-spoof IP filter status
      description: Whether the per-guest anti-spoof IP filter is on, plus its allow-list and the guest's assigned CIDRs.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
        - $ref: '#/components/parameters/NodeQuery'
      responses:
        '200': { description: 'Filter status and allow-list.', content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { $ref: '#/components/responses/BadGateway' }
    post:
      operationId: setGuestIpFilter
      tags: [Guests]
      summary: Toggle the anti-spoof IP filter
      description: Enabling requires the guest to already hold an assigned IP; the allow-list is the union of every IP assigned to the guest.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [node]
              properties:
                node: { type: string }
                enable: { type: boolean }
      responses:
        '200': { description: Toggled., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { $ref: '#/components/responses/BadGateway' }
  /hosts/{hostId}/guests/{type}/{vmid}/ips:
    get:
      operationId: listGuestIps
      tags: [Guests]
      summary: List the guest's assigned IPs
      description: Every IP assigned to the guest (its primary plus any customer-pool IPs) and the resulting CIDR set.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
      responses:
        '200': { description: Assigned IPs., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    post:
      operationId: assignGuestIp
      tags: [Guests]
      summary: Assign an IP to the guest
      description: Binds an unassigned IP address row (`ip_id`) to the guest and re-syncs the anti-spoof allow-list. The IP must belong to the same host.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ip_id]
              properties:
                ip_id: { type: integer }
                node: { type: string, description: 'Optional; helps resync when the guest is not yet in inventory.' }
      responses:
        '200': { description: 'Assigned (reboot to apply).', content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Already assigned.', content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      operationId: unassignGuestIp
      tags: [Guests]
      summary: Unassign an IP from the guest
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
        - { name: ip_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200': { description: Unassigned., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /hosts/{hostId}/guests/{type}/{vmid}/events:
    get:
      operationId: listGuestEvents
      tags: [Guests]
      summary: Guest activity log
      description: The DB-backed per-guest activity log, newest first. Paginated.
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200': { description: Events., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /hosts/{hostId}/guests/{type}/{vmid}/firewall/rules:
    get:
      operationId: listGuestFirewallRules
      tags: [Firewall]
      summary: List per-guest firewall rules
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
        - $ref: '#/components/parameters/NodeQuery'
      responses:
        '200': { description: Rules., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    post:
      operationId: createGuestFirewallRule
      tags: [Firewall]
      summary: Add a per-guest firewall rule
      description: Persists a rule and reconciles it to the guest's PVE firewall (best-effort).
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [node, direction, action]
              properties:
                node: { type: string }
                direction: { type: string, enum: [in, out] }
                action: { type: string, enum: [ACCEPT, DROP, REJECT] }
                proto: { type: string }
                dport: { type: string }
                sport: { type: string }
                source: { type: string }
                dest: { type: string }
                comment: { type: string }
                enabled: { type: boolean }
      responses:
        '200': { description: Created., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /hosts/{hostId}/guests/{type}/{vmid}/firewall/rules/{ruleId}:
    delete:
      operationId: deleteGuestFirewallRule
      tags: [Firewall]
      summary: Delete a per-guest firewall rule
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
        - { name: ruleId, in: path, required: true, schema: { type: integer } }
        - $ref: '#/components/parameters/NodeQuery'
      responses:
        '200': { description: Deleted., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /hosts/{hostId}/guests/{type}/{vmid}/firewall/options:
    get:
      operationId: getGuestFirewallOptions
      tags: [Firewall]
      summary: Get per-guest firewall options
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
        - $ref: '#/components/parameters/NodeQuery'
      responses:
        '200': { description: Options., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    post:
      operationId: setGuestFirewallOptions
      tags: [Firewall]
      summary: Set per-guest firewall options
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [node]
              properties:
                node: { type: string }
                enabled: { type: boolean }
                policy_in: { type: string, enum: [ACCEPT, DROP, REJECT] }
                policy_out: { type: string, enum: [ACCEPT, DROP, REJECT] }
      responses:
        '200': { description: Saved., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /hosts/{hostId}/nat:
    get:
      operationId: getNat
      tags: [Networking]
      summary: Get NAT settings and port-forwards
      parameters:
        - $ref: '#/components/parameters/HostId'
      responses:
        '200': { description: NAT config., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      operationId: setNatSettings
      tags: [Networking]
      summary: Set NAT settings
      parameters:
        - $ref: '#/components/parameters/HostId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [wan_iface, private_cidr]
              properties:
                enabled: { type: boolean }
                wan_iface: { type: string }
                private_cidr: { type: string, description: 'RFC1918 CIDR, for example 10.0.10.0/24.' }
                masquerade: { type: boolean }
      responses:
        '200': { description: Saved., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /hosts/{hostId}/nat/port-forwards:
    post:
      operationId: createPortForward
      tags: [Networking]
      summary: Add a port-forward
      description: Adds a DNAT port-forward. Validated fail-closed (dest IP must be inside the private CIDR).
      parameters:
        - $ref: '#/components/parameters/HostId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [proto, public_port, dest_ip, dest_port]
              properties:
                proto: { type: string, enum: [tcp, udp] }
                public_port: { type: integer }
                dest_ip: { type: string }
                dest_port: { type: integer }
                comment: { type: string }
      responses:
        '200': { description: Created., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Duplicate public port.', content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '422': { $ref: '#/components/responses/ValidationError' }
  /hosts/{hostId}/nat/port-forwards/{pfId}:
    delete:
      operationId: deletePortForward
      tags: [Networking]
      summary: Delete a port-forward
      parameters:
        - $ref: '#/components/parameters/HostId'
        - { name: pfId, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: Deleted., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /hosts/{hostId}/nat/apply:
    post:
      operationId: applyNat
      tags: [Networking]
      summary: Apply NAT
      description: Enqueues the constrained apply_nat agent job that writes the host's NAT rules from validated templates.
      parameters:
        - $ref: '#/components/parameters/HostId'
      responses:
        '200': { description: Applied/queued., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '502': { $ref: '#/components/responses/BadGateway' }
  /hosts/{hostId}:
    post:
      operationId: updateHost
      tags: [Hosts & nodes]
      summary: Update a host
      description: An empty or absent password leaves the stored one unchanged.
      parameters:
        - $ref: '#/components/parameters/HostId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                hostname: { type: string }
                port: { type: integer }
                username: { type: string }
                realm: { type: string }
                password: { type: string }
                verify_tls: { type: boolean }
      responses:
        '200': { description: Updated., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      operationId: deleteHost
      tags: [Hosts & nodes]
      summary: Delete a host
      description: Full DB purge of the host and all its per-host data. The on-host agent/storage wipe stays a panel action.
      parameters:
        - $ref: '#/components/parameters/HostId'
      responses:
        '200': { description: Deleted., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /hosts/{hostId}/provision-image-storage:
    post:
      operationId: provisionImageStorage
      tags: [Hosts & nodes]
      summary: Provision the nexovirt-images storage
      description: Enqueues the agent job that ensures the managed nexovirt-images storage on the node. Requires an active agent.
      parameters:
        - $ref: '#/components/parameters/HostId'
      responses:
        '200': { description: Provisioned., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { $ref: '#/components/responses/BadGateway' }
  /users/{userId}/ssh-keys/{keyId}:
    delete:
      operationId: deleteUserSshKey
      tags: [Users]
      summary: Delete one of a user's SSH keys
      parameters:
        - $ref: '#/components/parameters/UserId'
        - { name: keyId, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: Deleted., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /users/{userId}/ips:
    get:
      operationId: listUserIps
      tags: [Users]
      summary: List a customer's owned IP pool
      description: Every IP owned by the customer (assigned and free), enriched with zone name and family.
      parameters:
        - $ref: '#/components/parameters/UserId'
      responses:
        '200': { description: Owned IPs., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      operationId: grantUserIps
      tags: [Users]
      summary: Grant IPs to a customer's pool
      description: >
        Grant IPs into a customer's pool. `mode=zone` (default) reserves the next `count` free
        addresses from `zone_id`; `mode=specific` grants the given `ids[]` from `zone_id`.
      parameters:
        - $ref: '#/components/parameters/UserId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [zone_id]
              properties:
                zone_id: { type: integer }
                mode: { type: string, enum: [zone, specific], default: zone }
                count: { type: integer }
                ids: { type: array, items: { type: integer } }
      responses:
        '200': { description: Granted., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /ip-addresses/{ipId}:
    delete:
      operationId: revokeIp
      tags: [Users]
      summary: Revoke an owned IP back to the admin pool
      description: Only an unassigned owned IP can be revoked.
      parameters:
        - { name: ipId, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: Revoked., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /alerts:
    get:
      operationId: listAlerts
      tags: [Alerts]
      summary: List alerts
      description: Alert instances, newest first, filterable by status and severity. Paginated.
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [active, acknowledged, resolved] } }
        - { name: severity, in: query, schema: { type: string, enum: [info, warning, critical] } }
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200': { description: Alerts., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /alerts/{alertId}/ack:
    post:
      operationId: ackAlert
      tags: [Alerts]
      summary: Acknowledge an alert
      parameters:
        - { name: alertId, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: Acknowledged., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /alerts/{alertId}/resolve:
    post:
      operationId: resolveAlert
      tags: [Alerts]
      summary: Resolve an alert
      parameters:
        - { name: alertId, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: Resolved., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
  /alert-rules:
    get:
      operationId: listAlertRules
      tags: [Alerts]
      summary: List alert rules
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200': { description: Rules., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
    post:
      operationId: createAlertRule
      tags: [Alerts]
      summary: Create an alert rule
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, severity, channels, duration_minutes]
              properties:
                name: { type: string }
                type: { type: string, enum: [guest_down, node_unreachable, cpu_high, mem_high, disk_high, agent_unhealthy, plan_traffic, plan_expiry] }
                severity: { type: string, enum: [info, warning, critical] }
                channels: { type: array, items: { type: string, enum: [inbox, email, webhook] } }
                threshold: { type: number }
                duration_minutes: { type: integer }
                host_id: { type: integer, nullable: true }
                node: { type: string, nullable: true }
                guest_type: { type: string, nullable: true }
                vmid: { type: integer, nullable: true }
                cooldown_minutes: { type: integer, nullable: true }
                enabled: { type: boolean }
      responses:
        '200': { description: Created., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '422': { $ref: '#/components/responses/ValidationError' }
  /alert-rules/{ruleId}:
    post:
      operationId: updateAlertRule
      tags: [Alerts]
      summary: Update an alert rule
      parameters:
        - { name: ruleId, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, severity, channels, duration_minutes]
              properties:
                name: { type: string }
                type: { type: string, enum: [guest_down, node_unreachable, cpu_high, mem_high, disk_high, agent_unhealthy, plan_traffic, plan_expiry] }
                severity: { type: string, enum: [info, warning, critical] }
                channels: { type: array, items: { type: string, enum: [inbox, email, webhook] } }
                threshold: { type: number }
                duration_minutes: { type: integer }
                host_id: { type: integer, nullable: true }
                node: { type: string, nullable: true }
                guest_type: { type: string, nullable: true }
                vmid: { type: integer, nullable: true }
                cooldown_minutes: { type: integer, nullable: true }
                enabled: { type: boolean }
      responses:
        '200': { description: Updated., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      operationId: deleteAlertRule
      tags: [Alerts]
      summary: Delete an alert rule
      parameters:
        - { name: ruleId, in: path, required: true, schema: { type: integer } }
      responses:
        '200': { description: Deleted., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /resellers:
    get:
      operationId: listResellers
      tags: [Resellers (admin)]
      summary: List resellers
      description: Paginated list of `role=reseller` accounts. Never includes secrets. Enterprise-only.
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Resellers.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/ResellerSummary' }
              example:
                success: true
                error: null
                data:
                  - { id: 9, username: acme-hosting, email: ops@acme.example, is_active: 1, customers: 12, created_at: '2026-07-01 09:00:00' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      operationId: createReseller
      tags: [Resellers (admin)]
      summary: Create a reseller
      description: Creates a `role=reseller` account with a generated password, returned once. Enterprise-only.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, email]
              properties:
                username: { type: string }
                email: { type: string, format: email }
            example: { username: acme-hosting, email: ops@acme.example }
      responses:
        '200':
          description: Reseller created.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/GeneratedCredential' }
              example:
                success: true
                error: null
                data: { id: 9, username: acme-hosting, email: ops@acme.example, password: 'Xk29-generated-pw' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /resellers/{resellerId}:
    get:
      operationId: getReseller
      tags: [Resellers (admin)]
      summary: Get a reseller
      description: Detail plus assigned nodes, assigned IP zones, quota plan id, and current usage. Enterprise-only.
      parameters:
        - $ref: '#/components/parameters/ResellerId'
      responses:
        '200':
          description: Reseller detail.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example:
                success: true
                error: null
                data:
                  id: 9
                  username: acme-hosting
                  email: ops@acme.example
                  is_active: 1
                  reseller_plan_id: 2
                  created_at: '2026-07-01 09:00:00'
                  assigned_nodes: [{ host_id: 1, node: pve1 }]
                  assigned_zones: [{ id: 3, name: 'Public v4', cidr: '203.0.113.0/24' }]
                  usage: { customers: 12, guests: 34, cores: 40, memory_mb: 65536, disk_gb: 800 }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      operationId: updateReseller
      tags: [Resellers (admin)]
      summary: Update a reseller
      description: Updates username, email, and active status. Enterprise-only.
      parameters:
        - $ref: '#/components/parameters/ResellerId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, email]
              properties:
                username: { type: string }
                email: { type: string, format: email }
                is_active: { type: boolean }
            example: { username: acme-hosting, email: ops@acme.example, is_active: true }
      responses:
        '200': { description: Updated., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      operationId: deleteReseller
      tags: [Resellers (admin)]
      summary: Delete a reseller
      description: Refuses (`422`) while the reseller still has customers assigned. Enterprise-only.
      parameters:
        - $ref: '#/components/parameters/ResellerId'
      responses:
        '200': { description: Deleted., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /resellers/{resellerId}/reset-password:
    post:
      operationId: resetResellerPassword
      tags: [Resellers (admin)]
      summary: Reset a reseller's password
      description: Generates a new password, returned once. Enterprise-only.
      parameters:
        - $ref: '#/components/parameters/ResellerId'
      responses:
        '200':
          description: Password reset.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: true, error: null, data: { id: 9, password: 'Yk91-new-pw' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /resellers/{resellerId}/customers:
    post:
      operationId: assignResellerCustomer
      tags: [Resellers (admin)]
      summary: Link a customer to a reseller
      description: Quota-gated (`422` past the reseller's plan cap). Enterprise-only.
      parameters:
        - $ref: '#/components/parameters/ResellerId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [customer_id]
              properties: { customer_id: { type: integer } }
            example: { customer_id: 4 }
      responses:
        '200':
          description: Linked.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: true, error: null, data: { reseller_id: 9, customer_id: 4 } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      operationId: unassignResellerCustomer
      tags: [Resellers (admin)]
      summary: Unlink a customer from a reseller
      parameters:
        - $ref: '#/components/parameters/ResellerId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [customer_id]
              properties: { customer_id: { type: integer } }
            example: { customer_id: 4 }
      responses:
        '200':
          description: Unlinked.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: true, error: null, data: { unassigned: 4 } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /resellers/{resellerId}/plan:
    post:
      operationId: assignResellerPlan
      tags: [Resellers (admin)]
      summary: Assign a reseller quota plan
      parameters:
        - $ref: '#/components/parameters/ResellerId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reseller_plan_id]
              properties: { reseller_plan_id: { type: integer } }
            example: { reseller_plan_id: 2 }
      responses:
        '200':
          description: Assigned.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: true, error: null, data: { reseller_id: 9, reseller_plan_id: 2 } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      operationId: unassignResellerPlan
      tags: [Resellers (admin)]
      summary: Clear a reseller's quota plan
      parameters:
        - $ref: '#/components/parameters/ResellerId'
      responses:
        '200':
          description: Cleared.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: true, error: null, data: { reseller_id: 9, reseller_plan_id: null } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /resellers/{resellerId}/nodes:
    post:
      operationId: assignResellerNode
      tags: [Resellers (admin)]
      summary: Grant node access
      description: Grants the reseller access to a (host, node) pair for placement and API guest creation.
      parameters:
        - $ref: '#/components/parameters/ResellerId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [host_id, node]
              properties:
                host_id: { type: integer }
                node: { type: string }
            example: { host_id: 1, node: pve1 }
      responses:
        '200':
          description: Granted.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: true, error: null, data: { reseller_id: 9, host_id: 1, node: pve1 } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      operationId: unassignResellerNode
      tags: [Resellers (admin)]
      summary: Revoke node access
      parameters:
        - $ref: '#/components/parameters/ResellerId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [host_id, node]
              properties:
                host_id: { type: integer }
                node: { type: string }
            example: { host_id: 1, node: pve1 }
      responses:
        '200':
          description: Revoked.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: true, error: null, data: { reseller_id: 9, host_id: 1, node: pve1 } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /resellers/{resellerId}/zones:
    post:
      operationId: assignResellerZone
      tags: [Resellers (admin)]
      summary: Grant IP-zone access
      parameters:
        - $ref: '#/components/parameters/ResellerId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ip_zone_id]
              properties: { ip_zone_id: { type: integer } }
            example: { ip_zone_id: 3 }
      responses:
        '200':
          description: Granted.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: true, error: null, data: { reseller_id: 9, ip_zone_id: 3 } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      operationId: unassignResellerZone
      tags: [Resellers (admin)]
      summary: Revoke IP-zone access
      parameters:
        - $ref: '#/components/parameters/ResellerId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ip_zone_id]
              properties: { ip_zone_id: { type: integer } }
            example: { ip_zone_id: 3 }
      responses:
        '200':
          description: Revoked.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: true, error: null, data: { reseller_id: 9, ip_zone_id: 3 } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /reseller-plans:
    get:
      operationId: listResellerPlans
      tags: [Resellers (admin)]
      summary: List reseller quota plans
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: Reseller plans.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/ResellerPlan' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      operationId: createResellerPlan
      tags: [Resellers (admin)]
      summary: Create a reseller quota plan
      description: '`0` for any `max_*` cap means unlimited.'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ResellerPlanInput' }
            example: { name: Standard, description: 'Up to 20 customers', max_customers: 20, max_guests: 60, max_cores: 80, max_memory_mb: 131072, max_disk_gb: 2000, is_active: true }
      responses:
        '200':
          description: Created.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/ResellerPlan' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /reseller-plans/{planId}:
    post:
      operationId: updateResellerPlan
      tags: [Resellers (admin)]
      summary: Update a reseller quota plan
      parameters:
        - $ref: '#/components/parameters/ResellerPlanId'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ResellerPlanInput' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/ResellerPlan' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      operationId: deleteResellerPlan
      tags: [Resellers (admin)]
      summary: Delete a reseller quota plan
      description: Refuses (`422`) while a reseller still references the plan.
      parameters:
        - $ref: '#/components/parameters/ResellerPlanId'
      responses:
        '200': { description: Deleted., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  # --- Reseller API (separate server + token; every op below is reseller-subtree-scoped) ---

  /ping:
    servers:
      - url: https://{host}/api/reseller
        variables: { host: { default: your-panel-host } }
    get:
      operationId: resellerPing
      tags: ['Reseller: Customers']
      summary: Auth probe
      description: Verifies the reseller token and returns the calling reseller's id. Useful for a quick connectivity/credential check.
      security: [{ resellerBearerAuth: [] }]
      responses:
        '200':
          description: Token is valid.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: true, error: null, data: { pong: true, reseller_id: 7 } }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /customers:
    servers:
      - url: https://{host}/api/reseller
        variables: { host: { default: your-panel-host } }
    get:
      operationId: listResellerCustomers
      tags: ['Reseller: Customers']
      summary: List my customers
      security: [{ resellerBearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          description: The calling reseller's customers only.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/ResellerCustomerSummary' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      operationId: createResellerCustomer
      tags: ['Reseller: Customers']
      summary: Create a customer
      description: Creates a customer under the calling reseller. Quota-gated (`422` past the reseller's plan cap).
      security: [{ resellerBearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, email]
              properties:
                username: { type: string }
                email: { type: string, format: email }
            example: { username: bob, email: bob@example.com }
      responses:
        '200':
          description: Created.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Envelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/GeneratedCredential' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /customers/{customerId}:
    servers:
      - url: https://{host}/api/reseller
        variables: { host: { default: your-panel-host } }
    get:
      operationId: getResellerCustomer
      tags: ['Reseller: Customers']
      summary: Get a customer
      description: '`404` if this customer does not belong to the calling reseller.'
      security: [{ resellerBearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/CustomerId'
      responses:
        '200':
          description: Customer detail.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: true, error: null, data: { id: 4, username: bob, email: bob@example.com, is_active: 1 } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      operationId: updateResellerCustomer
      tags: ['Reseller: Customers']
      summary: Update a customer
      security: [{ resellerBearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/CustomerId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, email]
              properties:
                username: { type: string }
                email: { type: string, format: email }
                is_active: { type: boolean }
      responses:
        '200': { description: Updated., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    post:
      operationId: updateResellerCustomerPost
      tags: ['Reseller: Customers']
      summary: Update a customer (POST alias)
      description: Identical to `PATCH /customers/{customerId}`, for clients that cannot send PATCH.
      security: [{ resellerBearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/CustomerId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, email]
              properties:
                username: { type: string }
                email: { type: string, format: email }
                is_active: { type: boolean }
      responses:
        '200': { description: Updated., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      operationId: deleteResellerCustomer
      tags: ['Reseller: Customers']
      summary: Delete a customer
      description: Refuses (`422`) while the customer still owns guests.
      security: [{ resellerBearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/CustomerId'
      responses:
        '200': { description: Deleted., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /customers/{customerId}/reset-password:
    servers:
      - url: https://{host}/api/reseller
        variables: { host: { default: your-panel-host } }
    post:
      operationId: resetResellerCustomerPassword
      tags: ['Reseller: Customers']
      summary: Reset a customer's password
      security: [{ resellerBearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/CustomerId'
      responses:
        '200':
          description: Password reset.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: true, error: null, data: { id: 4, password: 'Yk91-new-pw' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /customers/{customerId}/ssh-keys:
    servers:
      - url: https://{host}/api/reseller
        variables: { host: { default: your-panel-host } }
    get:
      operationId: listResellerCustomerSshKeys
      tags: ['Reseller: SSH keys']
      summary: List a customer's SSH keys
      description: id/name/fingerprint only, never the key body.
      security: [{ resellerBearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/CustomerId'
      responses:
        '200':
          description: SSH keys.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: true, error: null, data: [{ id: 1, name: laptop, fingerprint: 'SHA256:abc...' }] }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      operationId: addResellerCustomerSshKey
      tags: ['Reseller: SSH keys']
      summary: Add an SSH key
      security: [{ resellerBearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/CustomerId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [public_key]
              properties:
                name: { type: string }
                public_key: { type: string }
            example: { name: laptop, public_key: 'ssh-ed25519 AAAA... bob@laptop' }
      responses:
        '200':
          description: Added.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example: { success: true, error: null, data: { id: 1, name: laptop, fingerprint: 'SHA256:abc...' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /customers/{customerId}/ssh-keys/{keyId}:
    servers:
      - url: https://{host}/api/reseller
        variables: { host: { default: your-panel-host } }
    delete:
      operationId: deleteResellerCustomerSshKey
      tags: ['Reseller: SSH keys']
      summary: Delete an SSH key
      security: [{ resellerBearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/CustomerId'
        - $ref: '#/components/parameters/SshKeyId'
      responses:
        '200': { description: Deleted., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }

  /guests:
    servers:
      - url: https://{host}/api/reseller
        variables: { host: { default: your-panel-host } }
    get:
      operationId: listResellerGuests
      tags: ['Reseller: Guests']
      summary: List my guests
      description: Guests owned by the calling reseller's own customers, across every node the reseller is assigned.
      security: [{ resellerBearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/NodeQuery'
        - name: type
          in: query
          required: false
          schema: { type: string, enum: [qemu, lxc] }
        - name: status
          in: query
          required: false
          schema: { type: string }
      responses:
        '200': { description: Guests., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      operationId: createResellerGuest
      tags: ['Reseller: Guests']
      summary: Create a guest for one of my customers
      description: >
        `owner_id` must be one of the calling reseller's own customers (`404` otherwise).
        `host_id`/`node` must be a (host, node) pair the reseller was granted; `plan_id` and
        any `ipv4_zone_id`/`ipv6_zone_id` must likewise belong to the reseller. Same shape as
        the admin `POST /hosts/{hostId}/guests` body, plus `owner_id`, `host_id`, and `node`.
      security: [{ resellerBearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [owner_id, host_id, node, type, vmid, storage]
              properties:
                owner_id: { type: integer, description: One of the calling reseller's own customer ids. }
                host_id: { type: integer }
                node: { type: string }
                type: { type: string, enum: [qemu, lxc] }
                vmid: { type: integer }
                name: { type: string }
                storage: { type: string }
                plan_id: { type: integer }
                cores: { type: integer }
                memory_mb: { type: integer }
                disk_gb: { type: integer }
                bridge: { type: string }
                os_template: { type: string, description: 'qemu `tpl:<id>` or lxc CT template volid.' }
                ip_source: { type: string, enum: [dhcp, zone] }
                ipv4_zone_id: { type: integer }
                ipv6_zone_id: { type: integer }
                password: { type: string }
              additionalProperties: true
            example: { owner_id: 4, host_id: 1, node: pve1, type: lxc, vmid: 210, name: bob-web, storage: local-lvm, plan_id: 2, ip_source: dhcp }
      responses:
        '200': { description: Create requested., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { $ref: '#/components/responses/BadGateway' }

  /guests/{hostId}/{type}/{vmid}:
    servers:
      - url: https://{host}/api/reseller
        variables: { host: { default: your-panel-host } }
    get:
      operationId: getResellerGuest
      tags: ['Reseller: Guests']
      summary: Get a guest
      description: '`404` unless owned by one of the calling reseller''s own customers.'
      security: [{ resellerBearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
      responses:
        '200': { description: Guest detail., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      operationId: deleteResellerGuest
      tags: ['Reseller: Guests']
      summary: Delete a guest
      security: [{ resellerBearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
        - name: node
          in: query
          required: true
          schema: { type: string }
      responses:
        '200': { description: Deleted., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { $ref: '#/components/responses/BadGateway' }

  /guests/{hostId}/{type}/{vmid}/power:
    servers:
      - url: https://{host}/api/reseller
        variables: { host: { default: your-panel-host } }
    post:
      operationId: powerResellerGuest
      tags: ['Reseller: Guests']
      summary: Power action
      security: [{ resellerBearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [node, action]
              properties:
                node: { type: string }
                action: { type: string, enum: [start, stop, shutdown, reboot, suspend, resume] }
            example: { node: pve1, action: reboot }
      responses:
        '200': { description: Action dispatched., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /guests/{hostId}/{type}/{vmid}/ip:
    servers:
      - url: https://{host}/api/reseller
        variables: { host: { default: your-panel-host } }
    post:
      operationId: changeResellerGuestIp
      tags: ['Reseller: Networking']
      summary: Change or release a guest's static IP
      description: >
        `action=change` requires `ip` (must fall inside the target zone's range) and
        assigns it; `action=release` frees the guest's current IP for that family. The
        target zone (`zone_id`, or the guest's current single-stack zone if omitted) must
        be one the calling reseller was granted. Reboot the guest to apply.
      security: [{ resellerBearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/HostId'
        - $ref: '#/components/parameters/GuestType'
        - $ref: '#/components/parameters/Vmid'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [node, action]
              properties:
                node: { type: string }
                action: { type: string, enum: [change, release] }
                zone_id: { type: integer }
                ip: { type: string, description: Required when action=change. }
            example: { node: pve1, action: change, zone_id: 3, ip: '203.0.113.42' }
      responses:
        '200': { description: Applied., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
        '502': { $ref: '#/components/responses/BadGateway' }

  /plans:
    servers:
      - url: https://{host}/api/reseller
        variables: { host: { default: your-panel-host } }
    get:
      operationId: listResellerPlansAvailable
      tags: ['Reseller: Plans & usage']
      summary: List plans available to me
      description: The reseller's own plans plus any global plans. Read-only.
      security: [{ resellerBearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200': { description: Plans., content: { application/json: { schema: { $ref: '#/components/schemas/Envelope' } } } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /usage:
    servers:
      - url: https://{host}/api/reseller
        variables: { host: { default: your-panel-host } }
    get:
      operationId: getResellerUsage
      tags: ['Reseller: Plans & usage']
      summary: My usage
      description: Per-customer usage breakdown plus the reseller's total usage and quota plan. Read-only.
      security: [{ resellerBearerAuth: [] }]
      responses:
        '200':
          description: Usage.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Envelope' }
              example:
                success: true
                error: null
                data:
                  customers: [{ id: 4, username: bob, guests: 3, cores: 6, memory_mb: 8192, disk_gb: 120 }]
                  usage: { customers: 12, guests: 34, cores: 40, memory_mb: 65536, disk_gb: 800 }
                  plan: { id: 2, name: Standard, max_customers: 20, max_guests: 60, max_cores: 80, max_memory_mb: 131072, max_disk_gb: 2000 }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >
        A NexoVirt API token, created in the panel under Settings, API tokens. Send it as
        `Authorization: Bearer <token>`.
    resellerBearerAuth:
      type: http
      scheme: bearer
      description: >
        A reseller API token, issued in the reseller portal under API tokens (separate from
        the admin `/api/v1` bearerAuth tokens above). Send it as `Authorization: Bearer
        <token>` against the `/api/reseller` server. Every endpoint is scoped to the
        calling reseller's own customers, guests, nodes, and IP zones; an id outside that
        subtree returns `404`, not `403`, so cross-tenant existence is never leaked.
  parameters:
    HostId:
      name: hostId
      in: path
      required: true
      description: The panel's internal host id (from `GET /hosts`).
      schema: { type: integer }
    UserId:
      name: userId
      in: path
      required: true
      schema: { type: integer }
    NodeName:
      name: node
      in: path
      required: true
      description: The PVE node name.
      schema: { type: string }
    ResellerId:
      name: resellerId
      in: path
      required: true
      description: The panel's internal user id of a `role=reseller` account.
      schema: { type: integer }
    ResellerPlanId:
      name: planId
      in: path
      required: true
      description: A reseller quota-plan id.
      schema: { type: integer }
    CustomerId:
      name: customerId
      in: path
      required: true
      description: The panel's internal user id of one of the calling reseller's own customers.
      schema: { type: integer }
    SshKeyId:
      name: keyId
      in: path
      required: true
      description: An SSH key id belonging to the given customer.
      schema: { type: integer }
    NodeQuery:
      name: node
      in: query
      required: false
      description: Restrict results to this PVE node (plus any datacenter-wide/shared entries).
      schema: { type: string }
    GuestType:
      name: type
      in: path
      required: true
      description: Guest type.
      schema: { type: string, enum: [qemu, lxc] }
    Vmid:
      name: vmid
      in: path
      required: true
      description: The guest's VMID.
      schema: { type: integer }
    Page:
      name: page
      in: query
      required: false
      description: Page number, 1-based.
      schema: { type: integer, minimum: 1, default: 1 }
    PerPage:
      name: perPage
      in: query
      required: false
      description: Items per page, clamped to 1..200.
      schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
    Range:
      name: range
      in: query
      required: false
      description: Time window for the series.
      schema: { type: string, enum: ['30m', '3h', '24h'], default: '30m' }
    Metrics:
      name: metrics
      in: query
      required: false
      description: Comma-separated subset of metrics to return, for example "cpu,mem". Defaults to all.
      schema: { type: string }
  responses:
    Unauthorized:
      description: Missing or invalid bearer token.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Envelope' }
          example: { success: false, data: null, error: 'Invalid or missing API token.' }
    Forbidden:
      description: The token is not scoped to this host, or its IP allowlist rejected the caller.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Envelope' }
          example: { success: false, data: null, error: 'This token is not permitted to access this host.' }
    NotFound:
      description: The host, guest, user, or job was not found.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Envelope' }
          example: { success: false, data: null, error: 'Host not found.' }
    ValidationError:
      description: The request failed validation, for example an invalid node name or missing field.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Envelope' }
          example: { success: false, data: null, error: 'Invalid node name.' }
    BadGateway:
      description: The underlying Proxmox VE call failed or the host was unreachable.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Envelope' }
          example: { success: false, data: null, error: 'Could not reach the host.' }
  schemas:
    Envelope:
      type: object
      required: [success, data, error]
      properties:
        success:
          type: boolean
        data:
          description: The response payload, shape depends on the endpoint. Null on error.
        error:
          description: A human-readable error message, null on success.
          type: [string, 'null']
        meta:
          $ref: '#/components/schemas/PaginationMeta'
    PaginationMeta:
      type: object
      properties:
        page: { type: integer, example: 1 }
        perPage: { type: integer, example: 50 }
        total: { type: integer, example: 137 }
    Host:
      type: object
      properties:
        id: { type: integer, example: 1 }
        name: { type: string, example: Test }
        hostname: { type: string, example: 188.40.137.121 }
        port: { type: integer, example: 8006 }
        user: { type: string, example: root@pam }
        active: { type: boolean, example: true }
    NodeSummary:
      type: object
      description: >
        A live PVE node summary as returned by Proxmox's own `nodes` endpoint, passed
        through with minimal shaping.
      properties:
        node: { type: string, example: pve1 }
        status: { type: string, example: online }
        cpu: { type: number, example: 0.03 }
        maxcpu: { type: integer, example: 8 }
        mem: { type: integer, example: 4294967296 }
        maxmem: { type: integer, example: 34359738368 }
        disk: { type: integer, example: 21474836480 }
        maxdisk: { type: integer, example: 107374182400 }
        uptime: { type: integer, example: 1234567 }
      additionalProperties: true
    NodeDetail:
      type: object
      description: >
        A live PVE node status object, passed through from Proxmox (CPU info, memory,
        root filesystem, load average, PVE version, and more). Field set varies by
        Proxmox VE version.
      additionalProperties: true
    Storage:
      type: object
      properties:
        id: { type: string, example: local-lvm, description: The PVE storage id. }
        node: { type: string, example: pve1 }
        type: { type: string, example: lvmthin }
        total: { type: integer, description: Bytes. }
        used: { type: integer, description: Bytes. }
        avail: { type: integer, description: Bytes. }
        used_pct: { type: number, example: 24.0 }
        content: { type: string, example: 'images,rootdir' }
        active: { type: boolean }
    NetworkInterface:
      type: object
      properties:
        iface: { type: string, example: vmbr0 }
        node: { type: string, example: pve1 }
        type: { type: string, example: bridge }
        role:
          type: string
          enum: [WAN, Private, Internal, Other]
        cidr: { type: string, example: 203.0.113.0/24 }
        gateway: { type: string, example: 203.0.113.1 }
        ports: { type: string, example: eno1 }
        active: { type: boolean }
    Plan:
      type: object
      properties:
        id: { type: integer, example: 2 }
        name: { type: string, example: Standard }
        description: { type: string }
        cores: { type: integer, example: 2 }
        memory_mb: { type: integer, example: 2048 }
        disk_gb: { type: integer, example: 20 }
        traffic_limit_gb: { type: integer, description: 'Monthly traffic limit. 0 means unlimited.' }
        runtime_days: { type: integer, description: 'Expiry in days. 0 means unlimited.' }
        grace_days: { type: integer, description: Grace period before auto-delete after expiry. }
        snapshot_limit: { type: integer }
    OsTemplate:
      type: object
      properties:
        id: { type: integer, example: 7 }
        ref: { type: string, example: 'tpl:7', description: The exact os_template value for a QEMU create. }
        name: { type: string, example: Ubuntu 24.04 }
        description: { type: string }
        ostype: { type: string, example: l26 }
        ci_user: { type: string, example: ubuntu }
        group_id: { type: integer }
        group_name: { type: string, example: General }
        is_shipped: { type: boolean }
    CtTemplateManaged:
      type: object
      properties:
        lxc_template_id: { type: integer, example: 3 }
        name: { type: string, example: Debian 12 }
        description: { type: string }
        ostype: { type: string, example: debian }
        arch: { type: string, example: amd64 }
        unprivileged: { type: boolean }
        group_id: { type: integer }
        source: { type: string, example: shipped }
    CtTemplateAvailable:
      type: object
      properties:
        volid: { type: string, example: 'local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst' }
        node: { type: string, example: pve1 }
        format: { type: string, example: tar.zst }
        size: { type: integer, description: Bytes. }
    CtTemplates:
      type: object
      properties:
        managed:
          type: array
          items: { $ref: '#/components/schemas/CtTemplateManaged' }
        available:
          type: array
          items: { $ref: '#/components/schemas/CtTemplateAvailable' }
    IpZone:
      type: object
      properties:
        id: { type: integer, example: 3 }
        ref: { type: string, example: 'zone:3', description: The exact ip_source value to allocate a static IP. }
        name: { type: string, example: Private LAN }
        family: { type: string, enum: [ipv4, ipv6] }
        bridge: { type: string, example: vmbr1 }
        cidr: { type: string, example: 10.10.0.0/24 }
        gateway: { type: string, example: 10.10.0.1 }
        vlan: { type: string }
        is_active: { type: boolean }
    SshKey:
      type: object
      properties:
        id: { type: integer, example: 5 }
        name: { type: string, example: laptop }
        fingerprint: { type: string, example: 'SHA256:9f0b6c...' }
    GuestListItem:
      type: object
      properties:
        host_id: { type: integer, example: 1 }
        host: { type: string, example: Test }
        node: { type: string, example: pve1 }
        vmid: { type: integer, example: 105 }
        name: { type: string, example: web-01 }
        type: { type: string, enum: [qemu, lxc] }
        status: { type: string, example: running }
        os: { type: string, example: l26, description: The PVE ostype. }
        ip: { type: string, example: 10.10.0.5 }
        cores: { type: integer, example: 2 }
        mem: { type: integer, description: Configured max memory in bytes. }
        storage: { type: integer, description: Configured max disk in bytes. }
        qemu_agent:
          type: [boolean, 'null']
          description: Whether the QEMU guest agent is enabled. Null for LXC or when unknown.
        tags:
          type: string
          description: PVE tags, semicolon-joined (empty string when none). Searchable in the panel.
          example: 'prod;web'
    GuestDetail:
      type: object
      properties:
        host_id: { type: integer, example: 1 }
        vmid: { type: integer, example: 105 }
        type: { type: string, enum: [qemu, lxc] }
        node: { type: string, example: pve1 }
        name: { type: string, example: web-01 }
        status: { type: string, example: running }
        ip: { type: string, example: 10.10.0.5 }
        cores: { type: integer, example: 2 }
        mem: { type: integer, description: Configured max memory in bytes. }
        disk: { type: integer, description: Configured max disk in bytes. }
        ostype: { type: string, example: l26 }
    GuestCreateRequest:
      type: object
      description: >
        Common fields apply to both types. QEMU additionally requires `os_template`. LXC
        requires exactly one of `lxc_template_id` or `os_template`. `password` is required
        unless at least one SSH key is present, either via `ssh_keys[]` or an `owner_id`
        with stored keys.
      required: [type, node, vmid]
      properties:
        type:
          type: string
          enum: [qemu, lxc]
        node:
          type: string
          description: Target PVE node name.
        vmid:
          type: integer
          description: A positive, free VMID. Get one from GET /hosts/{hostId}/next-vmid.
        name:
          type: string
          description: Guest name/hostname. Defaults to vm-<vmid> or ct-<vmid>.
        plan_id:
          type: integer
          description: A plan id from GET /hosts/{hostId}/plans. Fills and locks cores/memory_mb/disk_gb.
        cores:
          type: integer
          default: 1
          description: Explicit spec when no plan_id is given.
        memory_mb:
          type: integer
          default: 512
          description: Explicit spec when no plan_id is given.
        disk_gb:
          type: integer
          default: 8
          description: Explicit spec when no plan_id is given.
        storage:
          type: string
          description: Storage id for the root disk, from GET /hosts/{hostId}/storages.
        bridge:
          type: string
          default: vmbr0
          description: Network bridge, from GET /hosts/{hostId}/networks.
        os_template:
          type: string
          description: >
            Required for QEMU as tpl:<id> from GET /hosts/{hostId}/os-templates. For LXC,
            an alternative to lxc_template_id, a raw vztmpl volid from GET
            /hosts/{hostId}/ct-templates.
        lxc_template_id:
          type: integer
          description: LXC only, a managed CT template id from GET /hosts/{hostId}/ct-templates.
        ip_source:
          type: string
          default: dhcp
          description: 'dhcp (default) or zone:<id> from GET /hosts/{hostId}/ip-zones to allocate a static IP.'
        password:
          type: string
          minLength: 5
          description: Root password. Required unless an SSH key is present.
        ssh_keys:
          type: array
          items: { type: string }
          description: Public key bodies, merged with the owner's stored keys.
        owner_id:
          type: integer
          description: Assigns ownership. The owner's stored SSH keys are auto-injected.
        snapshot_limit:
          type: integer
          description: Max snapshots for this guest.
        firewall_presets:
          type: array
          items: { type: integer }
          description: Firewall preset ids to apply.
        extra_storage:
          type: array
          items: { type: string }
          description: Parallel array with extra_size/extra_mount, additional disk storage ids.
        extra_size:
          type: array
          items: { type: integer }
          description: Parallel array with extra_storage/extra_mount, additional disk sizes in GB.
        extra_mount:
          type: array
          items: { type: string }
          description: >
            Parallel array with extra_storage/extra_size, LXC-only mount paths. Must be
            absolute paths; blank defaults to /mnt/disk<N>.
        unprivileged:
          type: boolean
          description: >
            LXC only. Create an unprivileged container. Omitted keeps PVE's default
            (privileged). For Docker in an unprivileged container also set nesting + keyctl.
        nesting:
          type: boolean
          description: 'LXC only. Adds nesting=1 to the container features (run Docker / systemd / nested containers).'
        keyctl:
          type: boolean
          description: 'LXC only. Adds keyctl=1 (required for Docker inside an unprivileged container).'
        fuse:
          type: boolean
          description: 'LXC only. Adds fuse=1 (allow FUSE mounts inside the container).'
        mount:
          type: string
          description: 'LXC only. Comma/semicolon list of mount types to allow (allow-listed: nfs, cifs). Invalid types return 422.'
        bios:
          type: string
          enum: [seabios, ovmf]
          description: 'QEMU only. Firmware. ovmf (UEFI) also allocates an EFI disk on the chosen storage. Unrecognised values are ignored.'
        machine:
          type: string
          enum: [i440fx, q35]
          description: 'QEMU only. Machine type. Unrecognised values are ignored.'
        cpu:
          type: string
          enum: [kvm64, host, qemu64, x86-64-v2, x86-64-v2-AES, x86-64-v3, x86-64-v4]
          description: 'QEMU only. CPU type. Unrecognised values are ignored.'
        vga:
          type: string
          enum: [std, qxl, virtio, vmware, cirrus, serial0, none]
          description: 'QEMU only. Display adapter. Unrecognised values are ignored.'
    ProvisioningQueued:
      type: object
      properties:
        queued: { type: boolean, example: true }
        job_id: { type: integer, example: 42 }
        vmid: { type: integer, example: 105 }
    ProvisioningJob:
      type: object
      properties:
        id: { type: integer, example: 42 }
        kind: { type: string, example: create, description: 'One of create, reinstall, delete, power.' }
        type: { type: string, enum: [qemu, lxc] }
        vmid: { type: integer, example: 105 }
        node: { type: string, example: pve1 }
        status:
          type: string
          enum: [queued, running, done, failed]
        error: { type: string, description: Populated when status is failed. }
        attempts: { type: integer }
        created_at: { type: string, example: '2026-07-14 10:00:00' }
        started_at: { type: string, example: '2026-07-14 10:00:01' }
        finished_at: { type: string, example: '2026-07-14 10:00:42' }
    UsageSeries:
      type: object
      properties:
        range:
          type: string
          enum: ['30m', '3h', '24h']
        series:
          type: array
          items:
            type: object
            properties:
              name: { type: string, example: cpu }
              unit: { type: string, example: '%' }
              points:
                type: array
                items:
                  type: array
                  minItems: 2
                  maxItems: 2
                  items: {}
                description: '[epoch_ms, value] pairs.'
    UsageSummary:
      type: object
      properties:
        guests:
          type: object
          properties:
            up: { type: integer }
            down: { type: integer }
            total: { type: integer }
        cores: { type: integer }
        mem: { type: integer, description: Bytes. }
        disk: { type: integer, description: Bytes. }
    UserSummary:
      type: object
      properties:
        id: { type: integer, example: 4 }
        username: { type: string, example: alice }
        email: { type: string, example: alice@example.com }
        external_id: { type: [string, 'null'] }
        is_active: { type: integer, example: 1 }
        guests: { type: integer, description: Number of guests owned by this user. }
    UserCreated:
      type: object
      properties:
        id: { type: integer, example: 4 }
        username: { type: string, example: alice }
        email: { type: string, example: alice@example.com }
        external_id: { type: [string, 'null'] }
        password: { type: string, description: 'Generated password, shown once.' }
    GeneratedCredential:
      type: object
      description: An account created with a one-time generated password. No external_id on this surface.
      properties:
        id: { type: integer, example: 4 }
        username: { type: string, example: alice }
        email: { type: string, example: alice@example.com }
        password: { type: string, description: 'Generated password, shown once.' }
    ResellerCustomerSummary:
      type: object
      properties:
        id: { type: integer, example: 4 }
        username: { type: string, example: alice }
        email: { type: string, example: alice@example.com }
        is_active: { type: integer, example: 1 }
        guests: { type: integer, description: Number of guests owned by this customer. }
    ResellerSummary:
      type: object
      properties:
        id: { type: integer, example: 9 }
        username: { type: string, example: acme-hosting }
        email: { type: string, example: ops@acme.example }
        is_active: { type: integer, example: 1 }
        customers: { type: integer, description: Number of customers currently linked to this reseller. }
        created_at: { type: string, example: '2026-07-01 09:00:00' }
    ResellerPlanInput:
      type: object
      required: [name]
      properties:
        name: { type: string }
        description: { type: [string, 'null'] }
        max_customers: { type: integer, description: '0 = unlimited.' }
        max_guests: { type: integer, description: '0 = unlimited.' }
        max_cores: { type: integer, description: '0 = unlimited.' }
        max_memory_mb: { type: integer, description: '0 = unlimited.' }
        max_disk_gb: { type: integer, description: '0 = unlimited.' }
        is_active: { type: boolean }
    ResellerPlan:
      allOf:
        - $ref: '#/components/schemas/ResellerPlanInput'
        - type: object
          properties:
            id: { type: integer, example: 2 }
