{
  "openapi": "3.1.0",
  "info": {
    "title": "RareCloud API",
    "version": "1.0.0",
    "description": "RareCloud platform API. Auth via dashboard cookie session OR `Authorization: Bearer rc_pat_<...>` token. Tokens are issued from the dashboard at /account/api/.\n\nAll responses use the envelope shape: `{ \"ok\": true, \"data\": <T> }` on success, `{ \"ok\": false, \"error\": { \"code\": <ErrorCode>, \"message\": string } }` on failure.\n\nRate limits surface as `X-RateLimit-Limit / -Remaining / -Reset` headers. Default per-token: 60 rpm. Per route group (e.g. `/v1/billing/*`). 429 returns `Retry-After`.\n\nA temporarily busy backend (for example the database under a burst of requests) answers 503 `BACKEND_UNAVAILABLE` with `Retry-After`. Retry a read after that many seconds; retry a create only with the same `Idempotency-Key`. 500 `INTERNAL` is reserved for real faults.\n\nCreate requests (POST) accept an optional `Idempotency-Key` header: send a fresh value per operation and the same value on a retry, and the operation runs at most once (see the IdempotencyKey parameter).\n\nPer-resource API access: every resource has an API access switch in the console. When a customer turns it off, the resource is read-only for API tokens and agents: reads still work, but changes and credential reads return 403 `RESOURCE_PROTECTED` with a console link. Service, Domain and proxy objects carry `apiAccess` (`full` | `read_only`), and `GET /api-access` lists the read-only ones. Do not retry a `RESOURCE_PROTECTED` refusal: ask the human to turn API access on in the console. Tokens can never change the switch.",
    "contact": {
      "name": "RareCloud Support",
      "url": "https://rarecloud.io"
    }
  },
  "servers": [
    {
      "url": "https://api.rarecloud.io/v1",
      "description": "Production — dedicated API host"
    },
    {
      "url": "https://console.rarecloud.io/api/v1",
      "description": "Same-origin base (for in-dashboard Try-it)"
    }
  ],
  "tags": [
    {
      "name": "Limits",
      "description": "Quota limits and usage for your account"
    },
    {
      "name": "Sandboxes",
      "description": "AI code sandboxes: isolated, ephemeral execution environments (run code, shell commands, files, preview ports)"
    },
    {
      "name": "Account",
      "description": "User account profile, sub-users, password"
    },
    {
      "name": "Billing",
      "description": "Credit balance, invoices, vouchers"
    },
    {
      "name": "Tickets",
      "description": "Support tickets"
    },
    {
      "name": "Tokens",
      "description": "API token lifecycle (cookie auth only)"
    },
    {
      "name": "Catalog",
      "description": "Orderable products, plans, regions, images. Public (un-authed)."
    },
    {
      "name": "Orders",
      "description": "Your purchase records (read-only). Orders are placed via POST /services."
    },
    {
      "name": "Domains",
      "description": "Domain registration lifecycle: availability, TLD pricing, register/transfer/renew, nameservers, contacts, DNS."
    },
    {
      "name": "Services",
      "description": "Running resources: VPS, cloud VMs, proxies, hosting."
    },
    {
      "name": "Proxies",
      "description": "Residential proxies (US Static Residential ISP): order, list, proxy credentials, renew."
    },
    {
      "name": "Reserved IPs",
      "description": "Reservable public IPv4 addresses (floating IPs) you keep across VM rebuilds — move between cloud VMs, survive destroy, billed €2/mo until released."
    },
    {
      "name": "Object Storage",
      "description": "S3-compatible object storage: buckets, access keys, usage and public delivery."
    },
    {
      "name": "Registry",
      "description": "Private, per-customer Docker/OCI container registry. Not launched yet: until launch, every registry write (enable, tier change, close, credentials, cluster link/unlink/rotate, repository/tag/manifest deletion, the Kubernetes pull-secret manifest) answers 503 BACKEND_UNAVAILABLE, and the token endpoint issues no tokens. Reads of an account that already exists keep working; without one they answer the usual 404."
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Personal access token from /account/api/"
      },
      "cookieAuth": {
        "type": "apiKey",
        "in": "cookie",
        "name": "rc_session",
        "description": "Session cookie (browser only)"
      },
      "basicAuth": {
        "type": "http",
        "scheme": "basic",
        "description": "HTTP Basic for the Docker/OCI registry token endpoint only — username `<handle>+<credential name>`, password = the credential secret. Not accepted on any other route; those use bearerAuth/cookieAuth."
      }
    },
    "schemas": {
      "BillingAlertState": {
        "type": "object",
        "description": "Your billing alert: what you asked to be told about, where you stand against it, and whether it has crossed. Two thresholds share one alert, an amount and a number of days of runway; either may be null, never both. Uniformly EUR; the burn rate and the client-currency figures are on GET /billing/state.",
        "properties": {
          "configured": {
            "type": "boolean",
            "description": "False when you have no alert at all. runwayHours is then null, while monthToDateCents still reports this month's real accrued cloud usage."
          },
          "enabled": {
            "type": "boolean",
            "description": "An alert can be kept but switched off; a disabled alert never triggers and never emails."
          },
          "thresholdCents": {
            "type": "integer",
            "nullable": true,
            "description": "Spend threshold in EUR cents, or null when you set none."
          },
          "thresholdDays": {
            "type": "integer",
            "nullable": true,
            "description": "Runway threshold in days (1 to 60), or null when you set none."
          },
          "monthToDateCents": {
            "type": "integer",
            "description": "This calendar month's cloud usage accrued so far and not yet invoiced, EUR cents. Reported whether or not an alert is configured."
          },
          "runwayHours": {
            "type": "number",
            "nullable": true,
            "description": "Hours your balance lasts at your current burn. Null unless thresholdDays is set (runway is read live from the billing backend, so it is only fetched for an alert that needs it), and null when there is no burn, no balance, or the backend was unreachable. The burn rate itself is on GET /billing/state, in your own currency."
          },
          "triggered": {
            "type": "boolean",
            "description": "True while either threshold is crossed and the alert is enabled."
          },
          "triggeredBy": {
            "type": "string",
            "nullable": true,
            "enum": [
              "spend",
              "runway",
              null
            ],
            "description": "Which threshold crossed. `spend` is reported when both did."
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR"
            ],
            "description": "Every money figure on this object is EUR; convert for display if your account bills in another currency."
          }
        }
      },
      "RegistryAccount": {
        "type": "object",
        "description": "Your private container registry account — one per customer. The handle is customer-chosen and immutable: it is the image path prefix, `<hostname>/<handle>/<repo>`.",
        "properties": {
          "handle": {
            "type": "string",
            "pattern": "^[a-z0-9]{3,30}$",
            "description": "Customer-chosen, immutable. Becomes the image path prefix."
          },
          "hostname": {
            "type": "string",
            "description": "The registry hostname images are pushed to and pulled from, e.g. registry.rarestack.net."
          },
          "tier": {
            "type": "string",
            "enum": [
              "free",
              "starter",
              "premium",
              "enterprise"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "suspended",
              "closed"
            ],
            "description": "suspended: pushes are blocked, pulls keep working, for non-payment or abuse. closed: the account is being torn down."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "usage": {
            "description": "Present on GET /registry (absent on POST/PATCH's response). Null if the account has never been measured yet, or if the usage read failed and degraded gracefully, see push.reason \"unavailable\".",
            "oneOf": [
              {
                "$ref": "#/components/schemas/RegistryUsage"
              },
              {
                "type": "null"
              }
            ]
          },
          "quota": {
            "description": "Present on GET /registry (absent on POST/PATCH's response). Null only if the account's own tier string is unrecognised, should not happen in practice, every write path validates against the four known tiers.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/RegistryQuota"
              },
              {
                "type": "null"
              }
            ]
          },
          "push": {
            "description": "Present on GET /registry (absent on POST/PATCH's response).",
            "$ref": "#/components/schemas/RegistryPushState"
          },
          "clusters": {
            "description": "Present on GET /registry (absent on POST/PATCH's response). How many of the account's owned Kubernetes clusters are linked to this registry, and how many more could be (Container Registry, Plan 3, spec §6). Null if the read failed, a Gardener or database hiccup degrades gracefully instead of turning a working GET /registry into a 5xx.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/RegistryClusterCounts"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "handle",
          "hostname",
          "tier",
          "status",
          "createdAt"
        ]
      },
      "RegistryCredential": {
        "type": "object",
        "description": "A robot credential for docker login / pull / push. The secret is never included here — see RegistryCredentialCreated, the one-time create response.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9-]{1,30}$",
            "description": "Your label for the credential; also half of the login username, `<handle>+<name>`."
          },
          "scope": {
            "type": "string",
            "enum": [
              "pull",
              "push"
            ]
          },
          "username": {
            "type": "string",
            "description": "The docker login / HTTP Basic username: `<handle>+<name>`."
          },
          "expiresAt": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "When the credential stops working, or null if it never expires."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastUsedAt": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "The last time this credential authenticated to GET|POST /registry/token, or null if it never has."
          },
          "revokedAt": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "When the credential was revoked, or null while it is active."
          }
        },
        "required": [
          "id",
          "name",
          "scope",
          "username",
          "expiresAt",
          "createdAt",
          "lastUsedAt",
          "revokedAt"
        ]
      },
      "RegistryCredentialCreated": {
        "type": "object",
        "description": "The ONE-TIME create response: a RegistryCredential plus its secret. The secret is stored only as a hash and cannot be retrieved again — save it now.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9-]{1,30}$",
            "description": "Your label for the credential; also half of the login username, `<handle>+<name>`."
          },
          "scope": {
            "type": "string",
            "enum": [
              "pull",
              "push"
            ]
          },
          "username": {
            "type": "string",
            "description": "The docker login / HTTP Basic username: `<handle>+<name>`."
          },
          "expiresAt": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "When the credential stops working, or null if it never expires."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastUsedAt": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "Always null on creation."
          },
          "revokedAt": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "Always null on creation."
          },
          "secret": {
            "type": "string",
            "description": "The credential secret — the docker login / HTTP Basic password. Returned exactly once, here; never shown again and never retrievable."
          }
        },
        "required": [
          "id",
          "name",
          "scope",
          "username",
          "expiresAt",
          "createdAt",
          "lastUsedAt",
          "revokedAt",
          "secret"
        ]
      },
      "RegistryToken": {
        "type": "object",
        "description": "Docker Registry v2 / OCI distribution bearer token, returned by GET|POST /registry/token. Not wrapped in the {ok,data} envelope — this is the raw shape Docker/OCI clients expect.",
        "properties": {
          "token": {
            "type": "string",
            "description": "RS256 JWT bearer token, scoped to the granted repository actions. Docker clients send it as `Authorization: Bearer <token>` to the registry itself."
          },
          "access_token": {
            "type": "string",
            "description": "Identical to token. Some Docker/OCI clients read this field name instead of token; both are always present and equal."
          },
          "expires_in": {
            "type": "integer",
            "enum": [
              300
            ],
            "description": "Token lifetime in seconds. Fixed."
          },
          "issued_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "token",
          "access_token",
          "expires_in",
          "issued_at"
        ]
      },
      "RegistryTokenError": {
        "type": "object",
        "description": "Docker distribution error envelope returned by GET|POST /registry/token on failure. Not the API's usual {ok,error} shape — Docker/OCI clients parse errors[].",
        "properties": {
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "code": {
                  "type": "string",
                  "enum": [
                    "UNAUTHORIZED",
                    "UNSUPPORTED",
                    "TOOMANYREQUESTS",
                    "UNAVAILABLE"
                  ]
                },
                "message": {
                  "type": "string"
                }
              },
              "required": [
                "code",
                "message"
              ]
            }
          }
        },
        "required": [
          "errors"
        ]
      },
      "RegistryUsage": {
        "type": "object",
        "description": "The account's latest hourly metering sample (decimal GB, 1e9 bytes, NOT binary GiB). See RegistryAccount.usage for when this is null.",
        "properties": {
          "logicalBytes": {
            "type": "integer",
            "description": "Total logical size of every repository under this account's handle, in bytes (decimal GB = logicalBytes / 1e9). Logical size, not physical: a layer shared by multiple repositories is counted in full for each one, the industry-standard billing basis (ECR/GHCR)."
          },
          "repoCount": {
            "type": "integer",
            "description": "Number of repositories under this account's handle at the time of this sample."
          },
          "sampledAt": {
            "type": "string",
            "format": "date-time",
            "description": "When this sample was taken. The meter samples every account hourly."
          }
        },
        "required": [
          "logicalBytes",
          "repoCount",
          "sampledAt"
        ]
      },
      "RegistryQuota": {
        "type": "object",
        "description": "This account's storage quota and overage rate at its CURRENT tier.",
        "properties": {
          "quotaGb": {
            "type": "number",
            "description": "Decimal GB included in the account's current tier before overage starts."
          },
          "burstCeilingGb": {
            "type": "number",
            "description": "quotaGb × the tier's burst multiplier (the free tier's multiplier is 1, so it never bursts). A push is refused once usage reaches this ceiling, see RegistryPushState."
          },
          "overageCentsPerGbMonth": {
            "type": "number",
            "description": "Price per decimal GB-month above quotaGb, billed automatically while usage stays under the burst ceiling."
          }
        },
        "required": [
          "quotaGb",
          "burstCeilingGb",
          "overageCentsPerGbMonth"
        ]
      },
      "RegistryPushState": {
        "type": "object",
        "description": "Whether a `docker push` is currently allowed for this account, and why. Pulls are never affected by this, only the push scopes GET|POST /registry/token grants.",
        "properties": {
          "allowed": {
            "type": "boolean"
          },
          "reason": {
            "type": "string",
            "enum": [
              "ok",
              "burst",
              "over_ceiling",
              "balance_under_72h",
              "meter_stale_24h",
              "no_sample",
              "unavailable",
              "repo_limit"
            ],
            "description": "ok: at or under quota. burst: above quota but below the burst ceiling, allowed, overage accrues. over_ceiling: at/above the burst ceiling, refused. balance_under_72h: the spendable balance cannot cover 72h of the account's current accrual rate, refused. meter_stale_24h: no fresh usage sample in over 24h, refused (fail-safe; an operator alert fires before this point). no_sample: never metered yet (brand-new account), allowed, no usage sample yet, so quota and burst cannot be evaluated; the repository cap still applies through a live count. unavailable: the usage/tier read itself failed (e.g. a meter database hiccup), allowed (fails OPEN: a read failure that is not the account's fault must never block a push). repo_limit: this account's repository count is at or above its tier's limit; pushes to a repository that does not exist yet are refused, pushes to an existing repository are still allowed (an economic guard, not a security boundary)."
          }
        },
        "required": [
          "allowed",
          "reason"
        ]
      },
      "RegistryTierChange": {
        "type": "object",
        "description": "PATCH /registry request body: change the account's billing tier. An upgrade (new tier's quota ≥ current) is always allowed; a downgrade is refused (409) if the account's last measured usage exceeds the new tier's quota.",
        "properties": {
          "tier": {
            "type": "string",
            "enum": [
              "free",
              "starter",
              "premium",
              "enterprise"
            ]
          }
        },
        "required": [
          "tier"
        ]
      },
      "RegistryCloseResult": {
        "type": "object",
        "description": "DELETE /registry's response: the account is now closed, NOT deleted. It stays reopenable with the SAME handle (POST /registry) until deleteAfter.",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "closed"
            ]
          },
          "deleteAfter": {
            "type": "string",
            "format": "date-time",
            "description": "When the account's storage is permanently deleted if it is not reopened before then."
          }
        },
        "required": [
          "status",
          "deleteAfter"
        ]
      },
      "RegistryRepositorySummary": {
        "type": "object",
        "description": "One row of GET /registry/repositories's list.",
        "properties": {
          "name": {
            "type": "string",
            "description": "The repository's name relative to your handle, pass this as {repo} to the detail/delete/vulnerabilities endpoints below. Never carries your handle."
          },
          "path": {
            "type": "string",
            "description": "The full namespaced repository path, `<handle>/<name>`, combine with the account hostname for the pull/push address."
          },
          "sizeBytes": {
            "type": "integer",
            "description": "Total logical size of every tag in this repository, in bytes."
          },
          "lastPush": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "When this repository was last pushed to, or null if it has never been measured."
          },
          "newestTag": {
            "type": ["string", "null"],
            "description": "The most recently pushed tag, or null if the repository has no tags."
          }
        },
        "required": [
          "name",
          "path",
          "sizeBytes",
          "lastPush",
          "newestTag"
        ]
      },
      "RegistryCveCounts": {
        "type": "object",
        "description": "CVE counts for one tag, bucketed by severity, from a single capped scan page (never more than 500 findings counted, see GET .../tags/{tag}/vulnerabilities for the full, paginated list). `unavailable: true` means zot's CVE/search extension is disabled, every count is 0, NOT a real \"no vulnerabilities found\" answer.",
        "properties": {
          "critical": {
            "type": "integer"
          },
          "high": {
            "type": "integer"
          },
          "medium": {
            "type": "integer"
          },
          "low": {
            "type": "integer"
          },
          "unknown": {
            "type": "integer",
            "description": "Findings whose severity the scanner reported outside critical/high/medium/low."
          },
          "unavailable": {
            "type": "boolean"
          }
        },
        "required": [
          "critical",
          "high",
          "medium",
          "low",
          "unknown",
          "unavailable"
        ]
      },
      "RegistryTag": {
        "type": "object",
        "description": "One tag of a repository, as returned in GET /registry/repositories/{repo}'s tags array.",
        "properties": {
          "tag": {
            "type": "string"
          },
          "digest": {
            "type": "string",
            "pattern": "^sha256:[a-f0-9]{64}$",
            "description": "The manifest digest this tag currently points at. Pass this to DELETE .../manifests/{digest} to delete the underlying image rather than just this one tag."
          },
          "sizeBytes": {
            "type": "integer"
          },
          "pushedAt": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "platforms": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Platforms this tag's manifest covers, e.g. [\"linux/amd64\", \"linux/arm64\"] for a multi-arch image."
          },
          "cve": {
            "$ref": "#/components/schemas/RegistryCveCounts"
          }
        },
        "required": [
          "tag",
          "digest",
          "sizeBytes",
          "pushedAt",
          "platforms",
          "cve"
        ]
      },
      "RegistryRepository": {
        "type": "object",
        "description": "GET /registry/repositories/{repo}'s response: one repository's full tag list. An empty tags array means the repository does not exist OR currently holds no images, zot has no separate signal to tell the two apart, so both read the same way (see the {repo} path's own description for when a malformed/foreign name 404s instead).",
        "properties": {
          "path": {
            "type": "string",
            "description": "The full namespaced repository path, `<handle>/{repo}`."
          },
          "name": {
            "type": "string",
            "description": "Same as the {repo} you requested."
          },
          "tags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RegistryTag"
            }
          }
        },
        "required": [
          "path",
          "name",
          "tags"
        ]
      },
      "RegistryCve": {
        "type": "object",
        "description": "One vulnerability finding, as returned by GET .../tags/{tag}/vulnerabilities.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Scanner finding id, e.g. a CVE identifier."
          },
          "severity": {
            "type": "string",
            "description": "Free-form scanner severity string, e.g. \"CRITICAL\"/\"HIGH\"/\"MEDIUM\"/\"LOW\", see RegistryCveCounts for how these bucket."
          },
          "title": {
            "type": "string"
          },
          "packages": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "installed": {
                  "type": "string",
                  "description": "The installed package version this image carries."
                },
                "fixed": {
                  "type": "string",
                  "description": "The version that fixes this finding."
                }
              },
              "required": [
                "name",
                "installed",
                "fixed"
              ]
            }
          }
        },
        "required": [
          "id",
          "severity",
          "title",
          "packages"
        ]
      },
      "RegistryClusterCounts": {
        "type": "object",
        "description": "How many of the account's owned Kubernetes clusters are linked to the container registry, and how many more could be. Embedded in RegistryAccount.clusters (GET /registry only, Plan 3).",
        "properties": {
          "linked": {
            "type": "integer",
            "minimum": 0,
            "description": "Link rows of ANY status, including still linking/rotating/unlinking/error, the customer already spent a link action on it."
          },
          "linkable": {
            "type": "integer",
            "minimum": 0,
            "description": "Owned clusters minus linked, floored at 0."
          }
        },
        "required": [
          "linked",
          "linkable"
        ]
      },
      "RegistryClusterLink": {
        "type": "object",
        "description": "One Kubernetes cluster linked to your container registry (GET /registry/clusters).",
        "properties": {
          "serviceId": {
            "type": "string",
            "description": "The cluster's service id, the same id GET /services/{id} uses."
          },
          "clusterName": {
            "type": "string",
            "description": "Your cluster's display name, joined from your owned clusters list. Falls back to serviceId if the cluster can no longer be found (see status)."
          },
          "status": {
            "type": "string",
            "enum": [
              "linking",
              "active",
              "rotating",
              "unlinking",
              "error"
            ],
            "description": "linking: install is in progress, or queued for retry. active: installed and working. rotating: a credential rotation is in flight, the OLD credential still works until it finishes. unlinking: teardown is in progress. error: the last install/rotate/unlink attempt failed (see lastError for why, e.g. cluster_unreachable, install_failed, or the cluster genuinely could not be found) and is retried on the next hourly pass."
          },
          "installedAt": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "When the secret-syncer was last installed cleanly, or null before the first successful install."
          },
          "syncerImage": {
            "type": ["string", "null"],
            "description": "The digest-pinned secret-syncer image currently installed, or null before the first install."
          },
          "syncerReady": {
            "type": "boolean",
            "description": "Whether the secret-syncer Deployment last reported itself Available. This is a PROBED value, refreshed on link/rotate and roughly hourly by the reconciler, a syncer that crash-loops right after a successful install reports status \"active\" (the install itself succeeded) with syncerReady:false."
          },
          "syncerCheckedAt": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "When syncerReady was last (re)probed, or null if it never has been."
          },
          "lastError": {
            "type": ["string", "null"],
            "description": "A short, secret-free reason the last attempt did not complete, or null. Drawn from a fixed vocabulary (e.g. cluster_unreachable, kubeconfig_unavailable, install_failed (HTTP <status>)), never raw upstream error text, which could otherwise echo the pull secret."
          },
          "credentialName": {
            "type": ["string", "null"],
            "description": "The name of the pull credential this link's secret-syncer is currently using (e.g. \"k8s-<serviceId>\"), never the secret itself. Null before the first successful link (no credential minted yet). Persisted here so it survives a page reload; a link/rotate call's own response also echoes it once, at the moment it changes."
          }
        },
        "required": [
          "serviceId",
          "clusterName",
          "status",
          "installedAt",
          "syncerImage",
          "syncerReady",
          "syncerCheckedAt",
          "lastError",
          "credentialName"
        ]
      },
      "RegistryTierInfo": {
        "type": "object",
        "description": "One row of GET /registry/tiers's list: a billing tier's quota, price and general-availability flag.",
        "properties": {
          "tier": {
            "type": "string",
            "enum": [
              "free",
              "starter",
              "premium",
              "enterprise"
            ]
          },
          "quotaGb": {
            "type": "number",
            "description": "Decimal GB included in this tier before overage starts."
          },
          "burstCeilingGb": {
            "type": "number",
            "description": "quotaGb × this tier's burst multiplier. A push is refused once usage reaches this ceiling."
          },
          "monthlyCents": {
            "type": "object",
            "description": "The tier's monthly price, in whole cents, in each currency we bill in.",
            "properties": {
              "EUR": {
                "type": "integer"
              },
              "USD": {
                "type": "integer",
                "description": "Converted from EUR at the platform's current FX rate at read time, not frozen to any account."
              }
            },
            "required": [
              "EUR",
              "USD"
            ]
          },
          "overageCentsPerGbMonth": {
            "type": "number",
            "description": "Price per decimal GB-month above quotaGb, in EUR cents."
          },
          "available": {
            "type": "boolean",
            "description": "Whether this tier is generally available (its catalog plan row is not hidden). An unauthenticated caller never sees a row with this false: see GET /registry/tiers's own description."
          }
        },
        "required": [
          "tier",
          "quotaGb",
          "burstCeilingGb",
          "monthlyCents",
          "overageCentsPerGbMonth",
          "available"
        ]
      },
      "ObjectStorageAccount": {
        "type": "object",
        "description": "Your object storage account — one per customer. Carries the regions you can build in, the price card in force, the last measured usage and this month's accrued cost.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "provisioning",
              "active",
              "suspended",
              "deleting",
              "failed"
            ],
            "description": "provisioning: being prepared, usually seconds. suspended: reads and writes are blocked for non-payment; your data is kept and still billed. failed: preparation stopped — failureReason says where."
          },
          "regions": {
            "type": "array",
            "description": "The regions you may create buckets in, each with the S3 endpoint your client must sign for.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Region id, e.g. eu-central-1."
                },
                "label": {
                  "type": "string",
                  "description": "The city the region is in."
                },
                "endpoint": {
                  "type": "string",
                  "description": "S3 endpoint URL for this region."
                }
              },
              "required": [
                "id",
                "label",
                "endpoint"
              ]
            }
          },
          "handle": {
            "type": "string",
            "nullable": true,
            "description": "Your namespace, the immutable prefix every bucket of this account carries (bucket names are `<handle>-<name>`). null until you choose one with your first POST /object-storage/buckets; it also stays null for an account that was set up before namespaces existed, which never uses one."
          },
          "quotaTb": {
            "type": "integer",
            "description": "Storage ceiling in TB."
          },
          "plan": {
            "type": "string",
            "enum": [
              "payg",
              "committed",
              "external"
            ],
            "description": "payg bills the price card below. committed bills a flat monthly fee for a reserved capacity, agreed separately. external is billed separately, outside your cloud balance: nothing on this account is metered or charged to it."
          },
          "reservedTb": {
            "type": "integer",
            "nullable": true,
            "description": "Committed plans only: the capacity reserved for you, in TB. Null on payg and external."
          },
          "committedMonthlyCents": {
            "type": "integer",
            "nullable": true,
            "description": "Committed plans only: the flat monthly fee in cents, charged whether the reservation is used or not. Null on payg and external."
          },
          "usage": {
            "type": "object",
            "description": "The last measured account totals. Measured once a day, not live.",
            "properties": {
              "activeBytes": {
                "type": "integer",
                "nullable": true,
                "description": "Stored bytes."
              },
              "deletedBytes": {
                "type": "integer",
                "nullable": true,
                "description": "Storage deleted within the last 90 days. It is still billed — see the minimum-retention term."
              },
              "egressBytesMtd": {
                "type": "integer",
                "nullable": true,
                "description": "Bytes downloaded out of the platform this calendar month. Egress up to your stored volume is free."
              },
              "measuredAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When the usage figures were last measured. Null means not yet measured, never zero."
              }
            },
            "required": [
              "activeBytes",
              "deletedBytes",
              "egressBytesMtd",
              "measuredAt"
            ]
          },
          "pricing": {
            "type": "object",
            "description": "The price card in force for this account, in cents.",
            "properties": {
              "baseMonthlyCents": {
                "type": "integer",
                "description": "Monthly base fee."
              },
              "includedGb": {
                "type": "integer",
                "description": "Storage included in the base fee."
              },
              "gbMonthlyCents": {
                "type": "number",
                "description": "Per GB-month above the inclusion."
              },
              "egressCentsPerGb": {
                "type": "number",
                "description": "Per GB of egress above your stored volume."
              },
              "cdnBucketMonthlyCents": {
                "type": "integer",
                "description": "Monthly fee per public (CDN-delivered) bucket."
              },
              "cdnTrafficCentsPerGb": {
                "type": "number",
                "description": "Per GB of CDN traffic."
              },
              "cdnCentsPerMillionRequests": {
                "type": "integer",
                "description": "Per million CDN requests above the inclusion."
              },
              "cdnIncludedRequests": {
                "type": "integer",
                "description": "CDN requests included per public bucket per month."
              },
              "currency": {
                "type": "string",
                "enum": [
                  "EUR"
                ]
              }
            },
            "required": [
              "baseMonthlyCents",
              "includedGb",
              "gbMonthlyCents",
              "egressCentsPerGb",
              "cdnBucketMonthlyCents",
              "cdnTrafficCentsPerGb",
              "cdnCentsPerMillionRequests",
              "cdnIncludedRequests",
              "currency"
            ]
          },
          "currentMonthAccruedCents": {
            "type": "integer",
            "nullable": true,
            "description": "Uninvoiced cost accrued this calendar month, in cents. Never more than you will be invoiced. Null means the figure could not be measured on this request — not zero, and not that nothing has accrued."
          },
          "failureReason": {
            "type": "string",
            "nullable": true,
            "description": "Why preparation stopped, when status is failed."
          },
          "limits": {
            "type": "object",
            "properties": {
              "maxBuckets": {
                "type": "integer"
              },
              "maxKeys": {
                "type": "integer"
              }
            },
            "required": [
              "maxBuckets",
              "maxKeys"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "status",
          "regions",
          "quotaTb",
          "plan",
          "reservedTb",
          "committedMonthlyCents",
          "usage",
          "pricing",
          "currentMonthAccruedCents",
          "failureReason",
          "limits",
          "createdAt"
        ]
      },
      "ObjectStorageBucket": {
        "type": "object",
        "description": "One S3 bucket.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "The name you chose. Unique within your account."
          },
          "region": {
            "type": "string",
            "description": "Region id the bucket lives in."
          },
          "endpoint": {
            "type": "string",
            "nullable": true,
            "description": "S3 endpoint to configure in your client for this bucket. Null in the rare case that the bucket's region is no longer offered — the bucket is still listable and still deletable, it just has no address to hand out."
          },
          "bucketUrl": {
            "type": "string",
            "nullable": true,
            "description": "Virtual-hosted-style URL for the bucket itself — the address your client signs for. Null for the same reason as endpoint."
          },
          "status": {
            "type": "string",
            "enum": [
              "creating",
              "active",
              "deleting",
              "failed"
            ],
            "description": "creating and deleting are transient. failed means the bucket was refused upstream and does not exist."
          },
          "versioning": {
            "type": "boolean",
            "description": "Whether object versions are kept. Old versions count as stored data and are billed."
          },
          "public": {
            "type": "boolean",
            "description": "Whether the bucket is delivered publicly over the CDN."
          },
          "publicUrl": {
            "type": "string",
            "nullable": true,
            "description": "The public delivery hostname, or null when the bucket is private."
          },
          "usage": {
            "type": "object",
            "description": "The last measured bucket totals. Measured once a day, not live.",
            "properties": {
              "bytes": {
                "type": "integer",
                "nullable": true,
                "description": "Stored bytes."
              },
              "deletedBytes": {
                "type": "integer",
                "nullable": true,
                "description": "Storage deleted within the last 90 days. It is still billed — see the minimum-retention term."
              },
              "objects": {
                "type": "integer",
                "nullable": true,
                "description": "Object count."
              },
              "measuredAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When the usage figures were last measured. Null means not yet measured, never zero."
              }
            },
            "required": [
              "bytes",
              "deletedBytes",
              "objects",
              "measuredAt"
            ]
          },
          "imported": {
            "type": "boolean",
            "description": "True for a bucket that existed before your account was connected. It is listed and measured, but managed with your own S3 tools: versioning, public delivery, deletion and key scoping are refused."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "name",
          "region",
          "endpoint",
          "bucketUrl",
          "status",
          "versioning",
          "public",
          "publicUrl",
          "usage",
          "imported",
          "createdAt"
        ]
      },
      "ObjectStorageKey": {
        "type": "object",
        "description": "An S3 access key. The secret is returned once, by POST /object-storage/keys, and never again.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "Your label for the key."
          },
          "accessKeyId": {
            "type": "string",
            "description": "The S3 access key id."
          },
          "secretPreview": {
            "type": "string",
            "description": "The last few characters of the secret, so you can tell two keys apart. Never the secret."
          },
          "scope": {
            "type": "object",
            "description": "What the key may do, and where.",
            "properties": {
              "buckets": {
                "description": "\"*\" for every bucket in the account, or an explicit list of bucket ids.",
                "oneOf": [
                  {
                    "type": "string",
                    "enum": [
                      "*"
                    ]
                  },
                  {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "minItems": 1
                  }
                ]
              },
              "access": {
                "type": "string",
                "enum": [
                  "read",
                  "readwrite"
                ]
              }
            },
            "required": [
              "buckets",
              "access"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "revoked"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "revokedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the key was revoked, or null while it is active."
          }
        },
        "required": [
          "id",
          "name",
          "accessKeyId",
          "secretPreview",
          "scope",
          "status",
          "createdAt",
          "revokedAt"
        ]
      },
      "ObjectStorageKeyCreated": {
        "type": "object",
        "description": "The ONE-TIME create response: an ObjectStorageKey plus its secret. The secret is not stored anywhere and cannot be retrieved again — save it now.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "Your label for the key."
          },
          "accessKeyId": {
            "type": "string",
            "description": "The S3 access key id."
          },
          "secretPreview": {
            "type": "string",
            "description": "The last few characters of the secret, so you can tell two keys apart."
          },
          "scope": {
            "type": "object",
            "properties": {
              "buckets": {
                "description": "\"*\" for every bucket in the account, or an explicit list of bucket ids.",
                "oneOf": [
                  {
                    "type": "string",
                    "enum": [
                      "*"
                    ]
                  },
                  {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "minItems": 1
                  }
                ]
              },
              "access": {
                "type": "string",
                "enum": [
                  "read",
                  "readwrite"
                ]
              }
            },
            "required": [
              "buckets",
              "access"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "revoked"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "revokedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "secretAccessKey": {
            "type": "string",
            "description": "The S3 secret access key. Returned exactly once, here."
          }
        },
        "required": [
          "id",
          "name",
          "accessKeyId",
          "secretPreview",
          "scope",
          "status",
          "createdAt",
          "revokedAt",
          "secretAccessKey"
        ]
      },
      "ObjectStorageUsagePoint": {
        "type": "object",
        "description": "One UTC day of measured usage. A day the measurement did not run has no point at all — a gap, never a zero.",
        "properties": {
          "day": {
            "type": "string",
            "format": "date",
            "description": "The UTC day, YYYY-MM-DD."
          },
          "activeBytes": {
            "type": "integer",
            "description": "Stored bytes at the end of the day."
          },
          "deletedBytes": {
            "type": "integer",
            "description": "Storage deleted within the last 90 days. It is still billed — see the minimum-retention term."
          },
          "egressBytes": {
            "type": "integer",
            "description": "Bytes downloaded out of the platform that day. Measured per account: on a bucket series this is always 0."
          },
          "cdnBytes": {
            "type": "integer",
            "description": "Bytes served by the CDN that day."
          },
          "cdnRequests": {
            "type": "integer",
            "description": "CDN requests that day."
          }
        },
        "required": [
          "day",
          "activeBytes",
          "deletedBytes",
          "egressBytes",
          "cdnBytes",
          "cdnRequests"
        ]
      },
      "Sandbox": {
        "type": "object",
        "description": "An AI code sandbox — an isolated, ephemeral execution environment owned by the caller.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Sandbox id (also the execution target for run_code/commands/files)."
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "running",
              "terminated"
            ]
          },
          "template": {
            "type": "string",
            "description": "Template slug this sandbox was created from (python | node | base)."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "timeoutMs": {
            "type": "integer",
            "description": "Idle timeout in milliseconds; the sandbox terminates after this."
          },
          "mocked": {
            "type": "boolean",
            "description": "Present (true) only while the sandbox backend runs in mock mode (no cluster configured)."
          }
        },
        "required": [
          "id",
          "status",
          "template",
          "createdAt"
        ]
      },
      "SandboxExecResult": {
        "type": "object",
        "description": "Result of executing code or a shell command inside a sandbox.",
        "properties": {
          "stdout": {
            "type": "string"
          },
          "stderr": {
            "type": "string"
          },
          "exitCode": {
            "type": "integer"
          },
          "results": {
            "type": "array",
            "items": {},
            "description": "Rich outputs (charts, tables) — reserved; currently always empty."
          },
          "mocked": {
            "type": "boolean",
            "description": "Present (true) only in mock mode."
          }
        },
        "required": [
          "stdout",
          "stderr",
          "exitCode"
        ]
      },
      "ReverseDnsRecord": {
        "type": "object",
        "description": "The PTR state for a Cloud VM's primary public IPv4 address.",
        "additionalProperties": false,
        "required": [
          "ip",
          "hostname",
          "ttl"
        ],
        "properties": {
          "ip": {
            "type": "string",
            "format": "ipv4"
          },
          "hostname": {
            "type": ["string", "null"],
            "description": "Normalized hostname without a trailing dot, or null when no PTR exists."
          },
          "ttl": {
            "type": ["integer", "null"],
            "minimum": 60,
            "description": "PTR TTL in seconds, or null when no PTR exists."
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "AUTH_REQUIRED",
                  "AUTH_INVALID_CREDENTIALS",
                  "AUTH_TOKEN_EXPIRED",
                  "PERMISSION_DENIED",
                  "ACCOUNT_CLOSED",
                  "FORBIDDEN",
                  "RESOURCE_PROTECTED",
                  "REGISTRATION_CLOSED",
                  "NOT_FOUND",
                  "METHOD_NOT_ALLOWED",
                  "CONFLICT",
                  "INVALID_PARAM",
                  "RATE_LIMITED",
                  "NOT_IMPLEMENTED",
                  "BACKEND_UNAVAILABLE",
                  "BACKEND_BAD_RESPONSE",
                  "IDEMPOTENCY_IN_PROGRESS",
                  "IDEMPOTENCY_KEY_REUSED",
                  "INTERNAL"
                ]
              },
              "message": {
                "type": "string"
              },
              "details": {
                "type": "array",
                "description": "Field-level validation details, returned with INVALID_PARAM.",
                "items": {
                  "type": "object",
                  "properties": {
                    "field": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "field",
                    "message"
                  ]
                }
              }
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "ok",
          "error"
        ]
      },
      "Account": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "firstName": {
            "type": "string",
            "maxLength": 64
          },
          "lastName": {
            "type": "string",
            "maxLength": 64
          },
          "companyName": {
            "type": "string",
            "nullable": true,
            "maxLength": 128
          },
          "country": {
            "type": "string",
            "minLength": 2,
            "maxLength": 2
          },
          "city": {
            "type": "string",
            "nullable": true,
            "maxLength": 64
          },
          "address": {
            "type": "string",
            "nullable": true,
            "maxLength": 128
          },
          "postcode": {
            "type": "string",
            "nullable": true,
            "maxLength": 16
          },
          "phone": {
            "type": "string",
            "nullable": true,
            "maxLength": 32
          },
          "taxId": {
            "type": "string",
            "nullable": true,
            "maxLength": 32,
            "description": "Tax/VAT identification number (optional)."
          },
          "preferredCurrency": {
            "type": "string",
            "enum": [
              "EUR",
              "USD"
            ]
          },
          "fxRate": {
            "type": "number",
            "description": "Live EUR->preferredCurrency rate, for converting EUR-quoted catalog teasers in a client UI. EUR clients get 1. Optional."
          },
          "taxRatePercent": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "description": "Effective VAT rate (percent) applied to this client's invoices: 0 for tax-exempt / reverse-charge clients, else the tax-rule rate for the client's country. Prices elsewhere are NET; multiply by (1 + taxRatePercent/100) for the VAT-inclusive total. Optional: absent when the rate source is unavailable (fall back to 'VAT added at checkout')."
          },
          "language": {
            "type": "string",
            "enum": [
              "en",
              "ro"
            ]
          },
          "twoFactorEnabled": {
            "type": "boolean"
          },
          "emailVerified": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "email",
          "firstName",
          "lastName",
          "country",
          "preferredCurrency",
          "language",
          "twoFactorEnabled",
          "emailVerified"
        ]
      },
      "Money": {
        "type": "object",
        "properties": {
          "amount": {
            "type": "number"
          },
          "currency": {
            "type": "string",
            "enum": [
              "EUR",
              "USD"
            ]
          }
        },
        "required": [
          "amount",
          "currency"
        ]
      },
      "Invoice": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "number": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "sale",
              "deposit"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "unpaid",
              "paid",
              "cancelled",
              "refunded",
              "collections"
            ]
          },
          "issuedAt": {
            "type": "string",
            "format": "date-time"
          },
          "dueAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "paidAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "subtotal": {
            "$ref": "#/components/schemas/Money"
          },
          "tax": {
            "$ref": "#/components/schemas/Money"
          },
          "total": {
            "$ref": "#/components/schemas/Money"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "paymentMethod": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "CreditBalance": {
        "type": "object",
        "properties": {
          "available": {
            "$ref": "#/components/schemas/Money"
          },
          "pending": {
            "$ref": "#/components/schemas/Money"
          },
          "transactions": {
            "type": "array",
            "items": {
              "type": "object"
            }
          }
        }
      },
      "PaymentMethod": {
        "type": "object",
        "description": "A payment option. WHMCS exposes the enabled payment gateways (no stored-card vault), so entries with isGatewayOption are checkout-time options chosen at payment, not stored instruments.",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "card",
              "paypal",
              "bank",
              "crypto"
            ]
          },
          "label": {
            "type": "string"
          },
          "last4": {
            "type": "string"
          },
          "brand": {
            "type": "string"
          },
          "expiresAt": {
            "type": "string"
          },
          "isDefault": {
            "type": "boolean"
          },
          "isGatewayOption": {
            "type": "boolean",
            "description": "True when this is an available checkout gateway rather than a stored instrument."
          },
          "payOnSpotOnly": {
            "type": "boolean",
            "description": "True for gateways (e.g. crypto) that can settle an invoice but cannot be saved as a reusable method."
          }
        },
        "required": [
          "id",
          "type",
          "label"
        ]
      },
      "Campaign": {
        "type": "object",
        "description": "The currently-active credit campaign (deposit-match promotion), public shape — feeds the 'double your credits' banner on Add Funds.",
        "properties": {
          "name": {
            "type": "string"
          },
          "endsAt": {
            "type": "string",
            "format": "date-time"
          },
          "tiers": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "minCents": {
                  "type": "integer"
                },
                "multiplier": {
                  "type": "number"
                },
                "bonusCents": {
                  "type": "integer"
                }
              },
              "required": [
                "minCents"
              ]
            }
          },
          "bonusExpiresDays": {
            "type": "integer",
            "nullable": true
          }
        },
        "required": [
          "name",
          "endsAt",
          "tiers"
        ]
      },
      "Ticket": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "number": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "open",
              "awaiting-staff",
              "awaiting-client",
              "closed"
            ]
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high"
            ]
          },
          "department": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "messages": {
            "type": "array",
            "items": {
              "type": "object"
            }
          }
        }
      },
      "ApiToken": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "secret_preview": {
            "type": "string"
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "rate_limit_rpm": {
            "type": "integer"
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "CatalogProduct": {
        "type": "object",
        "properties": {
          "sku": {
            "type": "string",
            "example": "whmcs.kvm-servers-plus-vps"
          },
          "kind": {
            "type": "string",
            "enum": [
              "legacy_vps",
              "cloud_compute",
              "cloud_k8s",
              "cloud_volume",
              "cloud_network",
              "dedicated_server",
              "proxy",
              "hosting",
              "domain",
              "app_hosting"
            ]
          },
          "backend": {
            "type": "string",
            "enum": [
              "whmcs",
              "virtualizor",
              "openstack",
              "gardener"
            ]
          },
          "backend_external_id": {
            "type": "string"
          },
          "display_name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "active": {
            "type": "boolean"
          },
          "usable_for": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "compute",
                "k8s"
              ]
            },
            "description": "Cloud VM plans only: what the plan can run. \"compute\" = a cloud VM; \"k8s\" = a managed Kubernetes worker pool. G, C and M plans carry both; shared-CPU s-* plans are [\"compute\"] only and are refused for worker pools."
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "required": [
          "sku",
          "kind",
          "backend",
          "backend_external_id",
          "display_name",
          "active"
        ]
      },
      "CatalogProductCard": {
        "type": "object",
        "description": "Structured deploy-wizard \"card\" for a catalog product. A-1 whitelist: built only from public metadata + plan rows — the `sku` is the customer-facing (prefix-free) SKU, never the WHMCS pid / Nova flavor UUID.",
        "properties": {
          "sku": {
            "type": "string",
            "description": "Public, prefix-free SKU (never backend_external_id).",
            "example": "kvm-servers-plus-vps"
          },
          "kind": {
            "type": "string"
          },
          "display_name": {
            "type": "string"
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "category": {
            "type": "string",
            "nullable": true
          },
          "active": {
            "type": "boolean"
          },
          "popular": {
            "type": "boolean"
          },
          "tier": {
            "type": "string",
            "nullable": true,
            "example": "shared"
          },
          "specs": {
            "type": "object",
            "description": "Whitelisted public hardware facts (vcpus, ram_gb, disk_gb, transfer_tb). Never carries ids.",
            "additionalProperties": true
          },
          "pricing": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "cycle": {
                  "type": "string",
                  "example": "monthly"
                },
                "amount": {
                  "type": "integer",
                  "description": "Price in whole cents.",
                  "example": 999
                },
                "currency": {
                  "type": "string",
                  "example": "EUR"
                }
              },
              "required": [
                "cycle",
                "amount",
                "currency"
              ]
            }
          },
          "available_regions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "sku",
          "kind",
          "display_name",
          "active",
          "pricing",
          "specs",
          "available_regions"
        ]
      },
      "CatalogPlan": {
        "type": "object",
        "properties": {
          "sku": {
            "type": "string",
            "example": "whmcs.kvm-servers-plus-vps.default"
          },
          "product_sku": {
            "type": "string"
          },
          "vcpu": {
            "type": "integer",
            "nullable": true
          },
          "ram_mb": {
            "type": "integer",
            "nullable": true
          },
          "disk_gb": {
            "type": "integer",
            "nullable": true
          },
          "bandwidth_gb": {
            "type": "integer",
            "nullable": true
          },
          "billing_cycles": {
            "type": "object",
            "description": "Map of cycle name → { currency → { price_cents, currency } }. Cycles: monthly, quarterly, semiannually, annually, biennially, triennially (prepay track) or hourly (cloud track).",
            "additionalProperties": true
          },
          "billing_tracks": {
            "type": "array",
            "description": "Subset of ['prepay','cloud'] indicating which billing models the plan supports.",
            "items": {
              "type": "string",
              "enum": [
                "prepay",
                "cloud"
              ]
            }
          },
          "active": {
            "type": "boolean"
          }
        },
        "required": [
          "sku",
          "product_sku",
          "billing_cycles",
          "active"
        ]
      },
      "CatalogRegion": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "example": "frankfurt-de"
          },
          "display_name": {
            "type": "string"
          },
          "country_code": {
            "type": "string",
            "minLength": 2,
            "maxLength": 2
          },
          "city": {
            "type": "string",
            "nullable": true
          },
          "region_group": {
            "type": "string",
            "nullable": true,
            "enum": [
              "americas",
              "europe",
              "asia",
              null
            ]
          },
          "backend_support": {
            "type": "object",
            "additionalProperties": true
          },
          "active": {
            "type": "boolean"
          }
        },
        "required": [
          "slug",
          "display_name",
          "country_code",
          "active"
        ]
      },
      "CatalogImage": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "example": "ubuntu-24.04"
          },
          "display_name": {
            "type": "string"
          },
          "os_family": {
            "type": "string",
            "description": "OS family for distros (e.g. `ubuntu`, `debian`), or the literal `app` for a marketplace application image.",
            "example": "ubuntu"
          },
          "version": {
            "type": "string",
            "nullable": true
          },
          "available_backends": {
            "type": "array",
            "description": "Names of the backends that can serve this image (e.g. `[\"openstack\"]`). Backend-internal image ids are never exposed.",
            "items": {
              "type": "string"
            }
          },
          "summary": {
            "type": "string",
            "description": "Optional one-line display copy, present mainly on marketplace apps (`os_family: \"app\"`).",
            "example": "WordPress on a LAMP stack, ready to publish"
          },
          "logo_slug": {
            "type": "string",
            "description": "Optional icon key for the image tile.",
            "example": "wordpress"
          },
          "active": {
            "type": "boolean"
          }
        },
        "required": [
          "slug",
          "display_name",
          "os_family",
          "active"
        ]
      },
      "Service": {
        "type": "object",
        "properties": {
          "apiAccess": {
            "type": "string",
            "enum": [
              "full",
              "read_only"
            ],
            "description": "Per-resource API access. `full` (the default): API tokens can do everything their scopes allow. `read_only`: the customer switched API access off in the console, so API tokens and agents can list and read this resource but every change, and every read that returns a credential, is refused with 403 `RESOURCE_PROTECTED`. Only a console session can change it (`PUT .../api-access`)."
          },
          "id": {
            "type": "string",
            "example": "srv_01H8E9..."
          },
          "kind": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "productName": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "pending",
              "suspended",
              "hibernated",
              "terminating",
              "terminated",
              "fraud",
              "cancelled",
              "error",
              "deleting"
            ],
            "description": "`suspended`: stopped for an unpaid cloud invoice (or by an administrator, see `suspendedBy`); paying the invoice resumes it. `hibernated` (managed Kubernetes only): the cluster is scaled to zero on purpose, not for non-payment; nothing is owed. `terminating`: a delete was accepted and the resource is being torn down. `deleting`: a load balancer whose delete was accepted while it was still being built (or while Octavia was still applying a change); it disappears once the deletion finishes. Clients should treat a status they do not know as transitional: not active, and not an error."
          },
          "statusReason": {
            "type": "string",
            "description": "Why the service is in status `error`, in one customer-facing line, when the API can say it (for example a load balancer whose background build gave up, or one whose deletion failed: `Deletion failed, contact support`). Absent otherwise."
          },
          "capabilities": {
            "type": "object",
            "description": "Per-resource action availability, present on the single-resource detail response (GET /services/{id}) only.",
            "properties": {
              "resetRootPassword": {
                "type": "boolean",
                "description": "cloud-vm only: true when the VM booted with the qemu-guest-agent channel, so POST /services/{id}/actions/reset-password can rotate the root password live."
              }
            }
          },
          "suspendedReason": {
            "type": "string",
            "nullable": true,
            "description": "Why the service is suspended (verbatim, set on admin/abuse suspensions, including suspensions applied in WHMCS itself on legacy services). Shown with a contact-support prompt."
          },
          "suspendedBy": {
            "type": "string",
            "enum": [
              "admin",
              "billing"
            ],
            "nullable": true,
            "description": "Suspension source: 'admin' (abuse/legal — contact support) or 'billing' (credit exhausted — top up to resume)."
          },
          "billingMode": {
            "type": "string",
            "enum": [
              "subscription",
              "usage"
            ]
          },
          "billingCycle": {
            "type": "string",
            "nullable": true
          },
          "recurringPrice": {
            "$ref": "#/components/schemas/Money",
            "nullable": true
          },
          "currentMonthAccrued": {
            "$ref": "#/components/schemas/Money",
            "nullable": true
          },
          "bandwidthUsage": {
            "type": "object",
            "nullable": true,
            "description": "cloud-vm only. Present while VM egress metering is enabled: the traffic the VM sent on its public interface this calendar month (UTC) and its current plan's monthly allowance. GB are binary (1 TB = 1024 GB). Traffic above the allowance is billed as bandwidth overage at EUR 1 per TB. Measured hourly, so the figure trails real time by up to about two hours. Absent when metering is off or the figure could not be read.",
            "properties": {
              "usedGb": {
                "type": "number",
                "description": "GB sent this calendar month, over the hours measured so far.",
                "example": 128.5
              },
              "includedGb": {
                "type": "number",
                "nullable": true,
                "description": "The monthly allowance of the VM's current plan in GB; null when unknown (no overage is billed then).",
                "example": 1024
              },
              "periodStart": {
                "type": "string",
                "format": "date-time",
                "description": "Start of the calendar month the figure covers (UTC)."
              },
              "measuredThrough": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "End of the latest measured hour this month; null before the first one."
              }
            },
            "required": [
              "usedGb",
              "includedGb",
              "periodStart",
              "measuredThrough"
            ]
          },
          "bandwidthAlerts": {
            "type": "array",
            "description": "cloud-vm only, present beside bandwidthUsage. The traffic alerts this VM has fired this calendar month (UTC), oldest first: 'allowance' when it has sent all the traffic included in its plan, '3x' when it has sent three times that. Each fires at most once per VM per month and emails the account. Traffic is never slowed down or stopped; traffic above the allowance is billed at EUR 1 per TB. Empty when nothing fired this month; absent when metering is off or the alerts could not be read.",
            "items": {
              "type": "object",
              "properties": {
                "threshold": {
                  "type": "string",
                  "enum": [
                    "allowance",
                    "3x"
                  ],
                  "description": "allowance = the included traffic was reached; 3x = three times the included traffic was reached."
                },
                "firedAt": {
                  "type": "string",
                  "format": "date-time",
                  "description": "When the VM was first seen at or past the threshold this month (measured hourly)."
                }
              },
              "required": [
                "threshold",
                "firedAt"
              ]
            }
          },
          "nextDueDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "region": {
            "type": "string",
            "nullable": true
          },
          "ipv4": {
            "type": "string",
            "nullable": true
          },
          "ipv6": {
            "type": "string",
            "nullable": true
          },
          "additionalIps": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true,
            "description": "Additional public IPs beyond the primary (legacy VPS)"
          },
          "apiEndpoint": {
            "type": "string",
            "nullable": true,
            "description": "k8s API-server endpoint URL"
          },
          "hostname": {
            "type": "string",
            "nullable": true
          },
          "specs": {
            "type": "object",
            "properties": {
              "cpu": {
                "type": "string",
                "nullable": true
              },
              "ram": {
                "type": "string",
                "nullable": true
              },
              "disk": {
                "type": "string",
                "nullable": true,
                "description": "Cloud VMs: the actual size of the root disk (e.g. `50 GB`), read from the disk itself; the plan's disk only when the real size cannot be read."
              },
              "bandwidth": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Labels on the service. For a cloud VM: the tags you set (on create or with PUT /services/{id}/tags), always an array, empty when there are none. A Kubernetes worker node also carries the platform's `managed:kubernetes`, `k8s:<cluster>` and `k8s:worker` tags."
          },
          "category": {
            "type": "string",
            "enum": [
              "cloud-vm",
              "cloud-k8s",
              "cloud-volume",
              "cloud-network",
              "cloud-loadbalancer",
              "cloud-reserved-ip",
              "cloud-object-storage",
              "cloud-registry",
              "server",
              "hosting",
              "proxy",
              "domain"
            ],
            "description": "The kind of service: a cloud resource type, a legacy VPS (`server`), cPanel `hosting`, a `proxy` or a `domain`. Clients should treat a category they do not know as an opaque service."
          },
          "powerStatus": {
            "type": "string",
            "enum": [
              "running",
              "stopped",
              "unknown"
            ],
            "description": "Live power state of the underlying VM (legacy VPS or cloud VM), distinct from the billing `status`. `unknown` when the hypervisor could not be reached. Absent for resources without a power state."
          },
          "usageRate": {
            "type": "object",
            "nullable": true,
            "description": "Hourly rate for usage-billed services. Reads zero while `prepaidUntil` is set.",
            "properties": {
              "perHour": {
                "$ref": "#/components/schemas/Money"
              }
            },
            "required": [
              "perHour"
            ]
          },
          "privateIp": {
            "type": "string",
            "nullable": true,
            "description": "Internal tenant-network address (cloud VMs)."
          },
          "groupName": {
            "type": "string",
            "description": "WHMCS product group name (for example `KVM Servers`, `Windows RDP`); used to sub-categorise legacy services. Empty for cloud resources."
          },
          "highAvailability": {
            "type": "boolean",
            "description": "cloud-k8s only: true when the cluster has a Gardener high-availability control plane."
          },
          "migratedTo": {
            "type": "object",
            "nullable": true,
            "description": "category `server` (legacy VPS) only: set when this VPS was migrated to a cloud VM. Its presence is the terminal state: the VPS accepts no further actions.",
            "properties": {
              "serverId": {
                "type": "string",
                "description": "The id of the cloud VM it was migrated to."
              },
              "migratedAt": {
                "type": "string",
                "description": "When the migration happened (ISO 8601)."
              },
              "newIp": {
                "type": "string",
                "nullable": true,
                "description": "Public IP of the new cloud VM, when known."
              }
            },
            "required": [
              "serverId",
              "migratedAt"
            ]
          },
          "prepaidUntil": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "cloud-vm only: ISO instant until which this VM costs nothing because it came from a legacy VPS whose term was already paid. While set, `usageRate` reads zero. Absent when billed normally."
          },
          "rateAfterPrepaid": {
            "$ref": "#/components/schemas/Money",
            "nullable": true,
            "description": "cloud-vm only, set while `prepaidUntil` is active: the hourly rate that begins at `prepaidUntil`."
          },
          "hosting": {
            "type": "object",
            "nullable": true,
            "description": "category:'hosting' (cPanel) only: real account facts from WHMCS (never fabricated). Present only when the cPanel provisioning module returned a username (the account actually exists). Individual usage fields are omitted, not zeroed, when WHMCS did not report them. Units are WHMCS's native MB.",
            "properties": {
              "username": {
                "type": "string",
                "description": "Real cPanel account username."
              },
              "serverHostname": {
                "type": "string",
                "nullable": true,
                "description": "Real cPanel server hostname (the shared-hosting node), used for the honest connection card. FTP/SSH ports are intentionally NOT provided — shared-hosting shell access is not guaranteed and we do not guess."
              },
              "diskUsedMb": {
                "type": "number",
                "nullable": true
              },
              "diskLimitMb": {
                "type": "number",
                "nullable": true
              },
              "bwUsedMb": {
                "type": "number",
                "nullable": true
              },
              "bwLimitMb": {
                "type": "number",
                "nullable": true
              }
            },
            "required": [
              "username"
            ]
          }
        },
        "required": [
          "id",
          "kind",
          "name",
          "status",
          "billingMode",
          "createdAt"
        ]
      },
      "LimitsRow": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "current": {
            "type": "number"
          },
          "cap": {
            "type": "number",
            "nullable": true,
            "description": "null = unlimited"
          },
          "unit": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          }
        },
        "required": [
          "key",
          "label",
          "current"
        ]
      },
      "SshKey": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "fingerprint": {
            "type": "string"
          },
          "publicKey": {
            "type": "string",
            "description": "The OpenSSH public key material (type + base64 blob)."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "name",
          "fingerprint",
          "publicKey",
          "createdAt"
        ]
      },
      "Snapshot": {
        "type": "object",
        "description": "A point-in-time copy of a cloud VM's root disk, stored as a copy-on-write clone. Cloud VMs only.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "creating",
              "securing",
              "ready",
              "failed"
            ],
            "description": "creating: the copy has not been made yet. securing: already usable — you can launch a VM from it — while it is detached from the source disk in the background. ready: fully independent. failed: see error."
          },
          "usedBytes": {
            "type": ["integer", "null"],
            "description": "Real storage occupied, in bytes, and the basis for billing. null until measured, which happens once the snapshot is independent. Deliberately not 0 — an unmeasured snapshot is not a free one."
          },
          "sourceVmId": {
            "type": ["string", "null"],
            "format": "uuid"
          },
          "sourceVmName": {
            "type": ["string", "null"]
          },
          "minDiskGb": {
            "type": ["integer", "null"],
            "description": "Smallest disk a VM can have to boot from this snapshot."
          },
          "createdAt": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "error": {
            "type": ["string", "null"]
          }
        },
        "required": [
          "id",
          "name",
          "state",
          "usedBytes"
        ]
      },
      "QuotaRequest": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "approved",
              "auto_approved",
              "denied"
            ],
            "description": "auto_approved means it was under the automatic ceiling and the region had headroom; it is already in effect."
          },
          "requested": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "currentLimits": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Snapshot of the limits at the moment of asking."
          },
          "currentUsage": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            },
            "description": "Snapshot of usage at the moment of asking."
          },
          "granted": {
            "type": ["object", "null"],
            "additionalProperties": {
              "type": "integer"
            },
            "description": "What was actually granted; may be less than requested."
          },
          "reason": {
            "type": "string"
          },
          "decisionNote": {
            "type": ["string", "null"]
          },
          "whmcsTicketId": {
            "type": ["integer", "null"]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "status",
          "requested",
          "reason",
          "createdAt"
        ]
      },
      "QuotaLimitRow": {
        "type": "object",
        "properties": {
          "resource": {
            "type": "string",
            "enum": [
              "cores",
              "ram",
              "instances",
              "volumes",
              "gigabytes",
              "floatingip",
              "network",
              "router",
              "subnet",
              "port",
              "security_group"
            ]
          },
          "used": {
            "type": "number",
            "description": "Currently consumed, counting anything reserved. NaN when unavailable is true."
          },
          "limit": {
            "type": "number",
            "description": "Ceiling for this resource. -1 means unlimited."
          },
          "state": {
            "type": "string",
            "enum": [
              "ok",
              "warn",
              "danger",
              "unknown"
            ],
            "description": "warn from 80% of the limit, danger from 95%, unknown when the region could not be read."
          },
          "ratio": {
            "type": ["number", "null"],
            "description": "Fraction of the limit consumed (0..1); null when unlimited or unknown."
          },
          "unavailable": {
            "type": "boolean",
            "description": "The owning service could not be read. Not the same as zero usage."
          }
        },
        "required": [
          "resource",
          "used",
          "limit",
          "state",
          "ratio"
        ]
      },
      "PoolCeiling": {
        "type": "object",
        "description": "How far one worker pool can actually scale, and what stops it.",
        "properties": {
          "pool": {
            "type": "string"
          },
          "reachable": {
            "type": "integer",
            "description": "Nodes this pool can reach in total, given the room left in your quota."
          },
          "configured": {
            "type": "integer",
            "description": "The maximum node count set on the pool."
          },
          "limiting": {
            "type": ["string", "null"],
            "enum": [
              "cores",
              "ram",
              "instances",
              "volumes",
              "gigabytes",
              null
            ],
            "description": "The resource that runs out first, or null when the full maximum fits."
          }
        },
        "required": [
          "pool",
          "reachable",
          "configured",
          "limiting"
        ]
      },
      "ReservedIp": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "ip": {
            "type": "string",
            "description": "The reserved public (floating) IPv4 address."
          },
          "fipId": {
            "type": "string",
            "description": "The backing Neutron floating-IP id."
          },
          "region": {
            "type": "string"
          },
          "attached": {
            "type": "boolean",
            "description": "Whether the IP is currently attached to a VM."
          },
          "attachedServerId": {
            "type": "string",
            "nullable": true,
            "description": "The cloud VM id this IP is attached to, or null when detached."
          },
          "monthlyCents": {
            "type": "integer",
            "description": "Monthly price in cents (€2/mo)."
          },
          "accruedCents": {
            "type": "integer",
            "description": "Display-level accrued cost (hourly proration from createdAt)."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "ip",
          "fipId",
          "region",
          "attached",
          "attachedServerId",
          "monthlyCents",
          "accruedCents",
          "createdAt"
        ]
      },
      "FirewallRule": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "direction": {
            "type": "string",
            "enum": [
              "inbound",
              "outbound"
            ]
          },
          "protocol": {
            "type": "string",
            "enum": [
              "tcp",
              "udp",
              "icmp",
              "all"
            ]
          },
          "ethertype": {
            "type": "string",
            "enum": [
              "IPv4",
              "IPv6"
            ]
          },
          "portRangeMin": {
            "type": "integer",
            "nullable": true
          },
          "portRangeMax": {
            "type": "integer",
            "nullable": true
          },
          "remoteCidr": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          }
        },
        "required": [
          "id",
          "direction",
          "protocol",
          "ethertype"
        ]
      },
      "FirewallRuleInput": {
        "type": "object",
        "properties": {
          "direction": {
            "type": "string",
            "enum": [
              "inbound",
              "outbound"
            ]
          },
          "protocol": {
            "type": "string",
            "enum": [
              "tcp",
              "udp",
              "icmp",
              "all"
            ]
          },
          "portRangeMin": {
            "type": "integer",
            "minimum": 1,
            "maximum": 65535
          },
          "portRangeMax": {
            "type": "integer",
            "minimum": 1,
            "maximum": 65535
          },
          "remoteCidr": {
            "type": "string",
            "description": "CIDR notation e.g. 0.0.0.0/0"
          },
          "description": {
            "type": "string",
            "maxLength": 255,
            "description": "Free text kept on the rule and returned as `description` when the firewall is read."
          }
        },
        "required": [
          "direction",
          "protocol"
        ]
      },
      "Firewall": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "isDefault": {
            "type": "boolean"
          },
          "inboundCount": {
            "type": "integer"
          },
          "outboundCount": {
            "type": "integer"
          },
          "attachedServerIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "id",
          "name",
          "isDefault",
          "inboundCount",
          "outboundCount",
          "attachedServerIds"
        ]
      },
      "FirewallDetail": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "isDefault": {
            "type": "boolean"
          },
          "inbound": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FirewallRule"
            }
          },
          "outbound": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FirewallRule"
            }
          },
          "attachedServerIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "id",
          "name",
          "isDefault",
          "inbound",
          "outbound",
          "attachedServerIds"
        ]
      },
      "LbMember": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "address": {
            "type": "string",
            "description": "The member VM's private fixed IP."
          },
          "port": {
            "type": "integer"
          },
          "status": {
            "type": "string",
            "description": "Operating status (ONLINE/OFFLINE/NO_MONITOR/...)."
          }
        },
        "required": [
          "id",
          "address",
          "port",
          "status"
        ]
      },
      "Vpc": {
        "type": "object",
        "properties": {
          "apiAccess": {
            "type": "string",
            "enum": [
              "full",
              "read_only"
            ],
            "description": "Per-resource API access. `full` (the default): API tokens can do everything their scopes allow. `read_only`: the customer switched API access off in the console, so API tokens and agents can list and read this resource but every change, and every read that returns a credential, is refused with 403 `RESOURCE_PROTECTED`. Only a console session can change it (`PUT .../api-access`)."
          },
          "id": {
            "type": "string",
            "description": "Neutron network id."
          },
          "name": {
            "type": "string"
          },
          "cidr": {
            "type": "string",
            "description": "The VPC subnet, e.g. 10.1.0.0/16."
          },
          "isDefault": {
            "type": "boolean",
            "description": "True for the account's default VPC (cannot be deleted)."
          }
        },
        "required": [
          "id",
          "name",
          "cidr",
          "isDefault"
        ]
      },
      "ProductDetails": {
        "type": "object",
        "description": "Live product detail: the public product + its plans + (for WHMCS-backed legacy products) the live billing cycles and per-product config options.",
        "properties": {
          "product": {
            "$ref": "#/components/schemas/CatalogProduct"
          },
          "plans": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CatalogPlan"
            }
          },
          "cycles": {
            "type": "array",
            "description": "Billing cycles actually available for this product (live). Empty for cloud SKUs.",
            "items": {
              "type": "object",
              "properties": {
                "cycle": {
                  "type": "string",
                  "enum": [
                    "monthly",
                    "quarterly",
                    "semiannually",
                    "annually",
                    "biennially",
                    "triennially"
                  ]
                },
                "amount": {
                  "type": "number"
                },
                "currency": {
                  "type": "string"
                },
                "setup": {
                  "type": "number",
                  "description": "One-time setup fee for this cycle (0 when none)."
                }
              },
              "required": [
                "cycle",
                "amount",
                "currency"
              ]
            }
          },
          "configOptions": {
            "type": "array",
            "description": "The product's own configurable options (Region, Additional IPv4, CPU/RAM, VPN install, ...). Empty for cloud SKUs.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                },
                "type": {
                  "type": "integer",
                  "description": "1 dropdown, 2 radio, 3 yes/no, 4 quantity."
                },
                "minQty": {
                  "type": "integer"
                },
                "maxQty": {
                  "type": "integer"
                },
                "options": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "id": {
                        "type": "integer"
                      },
                      "name": {
                        "type": "string"
                      },
                      "prices": {
                        "type": "object",
                        "description": "Recurring price per available billing cycle.",
                        "additionalProperties": {
                          "type": "number"
                        }
                      },
                      "setup": {
                        "type": "object",
                        "description": "One-time setup fee per available billing cycle (0 when none). Added to the first invoice / \"due now\" total.",
                        "additionalProperties": {
                          "type": "number"
                        }
                      },
                      "disabled": {
                        "type": "boolean",
                        "description": "Closed for NEW orders (e.g. a retired location). Absent = selectable. An order selecting a disabled option is refused with INVALID_PARAM; existing services are unaffected."
                      },
                      "disabledReason": {
                        "type": "string",
                        "description": "Customer-facing reason the option is closed, present when disabled."
                      }
                    }
                  }
                }
              }
            }
          },
          "osFieldId": {
            "type": "integer",
            "description": "WHMCS custom-field id of the Operating System field, when the product has one."
          }
        },
        "required": [
          "product",
          "plans",
          "cycles",
          "configOptions"
        ]
      },
      "Order": {
        "type": "object",
        "description": "A purchase record. Created via POST /services; one order maps 1:1 to an invoice and produces one or more service lines.",
        "properties": {
          "id": {
            "type": "string"
          },
          "orderNumber": {
            "type": "string",
            "nullable": true
          },
          "date": {
            "type": "string",
            "nullable": true,
            "description": "Order placement timestamp (server-local 'YYYY-MM-DD HH:mm:ss')."
          },
          "status": {
            "type": "string",
            "nullable": true,
            "description": "Pending | Active | Fraud | Cancelled."
          },
          "paymentStatus": {
            "type": "string",
            "nullable": true,
            "description": "Paid | Unpaid | ..."
          },
          "invoiceId": {
            "type": "string",
            "nullable": true,
            "description": "The invoice this order generated."
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string",
                  "nullable": true
                },
                "product": {
                  "type": "string",
                  "nullable": true
                },
                "productType": {
                  "type": "string",
                  "nullable": true
                },
                "status": {
                  "type": "string",
                  "nullable": true
                },
                "serviceId": {
                  "type": "string",
                  "nullable": true,
                  "description": "The service id this product line created, if any."
                }
              }
            }
          }
        },
        "required": [
          "id",
          "items"
        ]
      },
      "Domain": {
        "type": "object",
        "description": "A domain registered (or registering) under the authenticated account.",
        "properties": {
          "apiAccess": {
            "type": "string",
            "enum": [
              "full",
              "read_only"
            ],
            "description": "Per-resource API access. `full` (the default): API tokens can do everything their scopes allow. `read_only`: the customer switched API access off in the console, so API tokens and agents can list and read this resource but every change, and every read that returns a credential, is refused with 403 `RESOURCE_PROTECTED`. Only a console session can change it (`PUT .../api-access`)."
          },
          "id": {
            "type": "string"
          },
          "domain": {
            "type": "string",
            "description": "The fully-qualified domain name."
          },
          "status": {
            "type": "string",
            "nullable": true,
            "description": "Active | Pending | Expired | Transferred Away | ..."
          },
          "registrationDate": {
            "type": "string",
            "nullable": true
          },
          "expiryDate": {
            "type": "string",
            "nullable": true
          },
          "registrar": {
            "type": "string",
            "nullable": true
          },
          "autoRenew": {
            "type": "boolean"
          },
          "idProtection": {
            "type": "boolean"
          },
          "registrationPeriod": {
            "type": "integer",
            "nullable": true,
            "description": "Registration period in years."
          }
        },
        "required": [
          "id",
          "domain",
          "autoRenew",
          "idProtection"
        ]
      },
      "DomainAvailability": {
        "type": "object",
        "description": "Result of a pre-purchase availability lookup (WHOIS).",
        "properties": {
          "domain": {
            "type": "string"
          },
          "available": {
            "type": "boolean"
          },
          "premium": {
            "type": "boolean",
            "description": "True if the domain is a premium-priced name."
          }
        },
        "required": [
          "domain",
          "available"
        ]
      },
      "TldPrice": {
        "type": "object",
        "description": "Per-TLD pricing (1-year, in the account currency).",
        "properties": {
          "tld": {
            "type": "string",
            "description": "TLD without the leading dot, e.g. 'com'."
          },
          "register": {
            "type": "number",
            "nullable": true
          },
          "transfer": {
            "type": "number",
            "nullable": true
          },
          "renew": {
            "type": "number",
            "nullable": true
          },
          "currency": {
            "type": "string",
            "description": "ISO currency code, e.g. 'EUR'."
          }
        },
        "required": [
          "tld",
          "currency"
        ]
      },
      "DomainContact": {
        "type": "object",
        "description": "Registrant WHOIS contact for a domain (registrar-dependent).",
        "properties": {
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "organisation": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "address1": {
            "type": "string"
          },
          "address2": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "postcode": {
            "type": "string"
          },
          "country": {
            "type": "string",
            "description": "2-letter ISO country code."
          },
          "phone": {
            "type": "string"
          }
        }
      },
      "DnsRecord": {
        "type": "object",
        "description": "A DNS host record (registrar-dependent — not all registrars expose DNS via API).",
        "properties": {
          "hostname": {
            "type": "string",
            "description": "Record name, e.g. '@', 'www', 'mail'."
          },
          "type": {
            "type": "string",
            "description": "A | AAAA | CNAME | MX | TXT | NS | SRV | CAA."
          },
          "address": {
            "type": "string",
            "description": "Record value / target."
          },
          "priority": {
            "type": "integer",
            "description": "For MX/SRV records."
          }
        },
        "required": [
          "hostname",
          "type",
          "address"
        ]
      },
      "DomainOrderResult": {
        "type": "object",
        "description": "Result of a register/transfer order. WHMCS has no 'register now' API — purchases are order-driven; the registrar module fulfils on accept/payment.",
        "properties": {
          "orderId": {
            "type": "string"
          },
          "domainId": {
            "type": "string",
            "nullable": true
          },
          "invoiceId": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "description": "Always 'Pending' at placement; fulfilment is async."
          }
        },
        "required": [
          "orderId",
          "status"
        ]
      }
    },
    "responses": {
      "ResourceProtected": {
        "description": "The resource is read-only for API tokens and agents (its API access switch is off in the console). Error code `RESOURCE_PROTECTED`; the message ends with a console link to the resource. Do not retry: a human must turn API access on in the console.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid auth",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Token lacks required scope",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BackendUnavailable": {
        "description": "BACKEND_UNAVAILABLE: an upstream service could not be read right now. This says nothing about whether the resource exists (it is never answered as 404). Retry after Retry-After seconds.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "IdempotencyInProgress": {
        "description": "IDEMPOTENCY_IN_PROGRESS: a request with the same Idempotency-Key is still running. Retry after Retry-After seconds with the same key.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer",
              "example": 5
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "IdempotencyKeyReused": {
        "description": "IDEMPOTENCY_KEY_REUSED: this Idempotency-Key was already used for a different request (another body or path). Use a new key for a new request.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Optional. A unique value you generate for this create (a UUIDv4 is ideal) and send again, unchanged, when you retry the SAME request after a timeout or a dropped connection. The operation runs at most once per account, route and key; a retry gets the original status and body back with `Idempotent-Replayed: true`. Credential fields (for example consolePassword, secretAccessKey, proxy passwords, kubeconfig) are shown only in the first answer: a replay omits them and lists them in `secretsOmittedOnReplay`. Keys are kept for 24 hours. Errors 5xx are not stored, so a retry after a 5xx runs the request again. Without the header the request behaves exactly as before.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 255,
          "pattern": "^[\\x20-\\x7E]+$"
        },
        "example": "5f0c6c1e-8a43-4b8e-9f2a-0d4a2f6e9b17"
      }
    },
    "headers": {
      "PartialResults": {
        "description": "Present only when the list is PARTIAL: a comma-separated list of the service categories whose backend could not be read for this response (for example `cloud-vm` while the compute service is briefly unavailable). Those resources are not deleted and are simply missing from this answer; retry in a minute for the full list. Absent when nothing is missing. The body shape is unchanged.",
        "schema": {
          "type": "string",
          "example": "cloud-vm"
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer",
          "example": 2
        }
      },
      "IdempotentReplayed": {
        "description": "Present (value `true`) only when this answer is a replay of an earlier request with the same Idempotency-Key.",
        "schema": {
          "type": "string",
          "enum": [
            "true"
          ]
        }
      }
    }
  },
  "security": [
    {
      "bearerAuth": []
    },
    {
      "cookieAuth": []
    }
  ],
  "paths": {
    "/registry": {
      "post": {
        "tags": [
          "Registry"
        ],
        "summary": "Enable the container registry, reopen a closed one, or re-enable a purged one",
        "description": "Enable your private container registry with a customer-chosen, immutable handle. The handle becomes your image path prefix: `<hostname>/<handle>/<repo>`. If your account was previously CLOSED (DELETE /registry) and is still inside its grace window, posting the SAME handle again REOPENS it instead (200, not 201), its storage was never deleted. If the grace window elapsed and the account was PURGED (storage already deleted for good), posting again RE-ENABLES it (200): the same handle is reclaimed (still reserved to you), or a different handle may be supplied if it is currently free (the old namespace holds nothing to protect). Requires scope `services:write`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "handle": {
                    "type": "string",
                    "pattern": "^[a-z0-9]{3,30}$",
                    "description": "3-30 lowercase letters/digits. Immutable once set, EXCEPT when re-enabling a CLOSED and PURGED account, where a different handle may be supplied if it is currently free. Some words are reserved."
                  },
                  "tier": {
                    "type": "string",
                    "enum": [
                      "free",
                      "starter",
                      "premium",
                      "enterprise"
                    ]
                  }
                },
                "required": [
                  "handle",
                  "tier"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Either: (a) reopened a previously CLOSED account with the same handle, still inside its grace window, its storage was never deleted; or (b) re-enabled a CLOSED and PURGED account (grace elapsed, storage already deleted for good) onto the same handle (reclaimed) or a different, currently-free handle supplied in the request. `data.tier` reflects the tier in THIS request on a re-enable (a fresh billing history), not whatever tier applied before the account closed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/RegistryAccount"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "201": {
            "description": "Registry enabled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/RegistryAccount"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "400": {
            "description": "Invalid handle: wrong shape or a reserved word",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Registry already enabled for this account, or the requested handle is already taken by someone else (error.details[0].message == \"handle_taken\"). Also returned for a CLOSED account that is neither reopenable (past its grace window, not yet purged) nor eligible for re-enable-after-purge with this request (for example, a DIFFERENT handle while still within the grace window), and for a SUSPENDED account (an operator action, never self-service). Also IDEMPOTENCY_IN_PROGRESS: a request with the same Idempotency-Key is still running; retry after Retry-After seconds with the same key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          },
          "503": {
            "description": "The container registry is not launched yet (error.code == \"BACKEND_UNAVAILABLE\"). Every registry write answers this until launch (error.message == \"The container registry is not available yet.\"), and nothing is changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      },
      "get": {
        "tags": [
          "Registry"
        ],
        "summary": "Get your container registry account",
        "description": "Your container registry account, including its live usage sample, current quota/burst ceiling, whether push is currently allowed (and why not, if it isn't), and how many of your owned Kubernetes clusters are linked (Plan 3). 404 if you have not enabled it yet. Requires scope `services:read`.",
        "responses": {
          "200": {
            "description": "OK. `usage`/`quota`/`push`/`clusters` are always present on this endpoint (unlike POST/PATCH's response, which omit them), see RegistryAccount.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "allOf": [
                        {
                          "$ref": "#/components/schemas/RegistryAccount"
                        },
                        {
                          "required": [
                            "usage",
                            "quota",
                            "push",
                            "clusters"
                          ]
                        }
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "The container registry is not enabled for this account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "patch": {
        "tags": [
          "Registry"
        ],
        "summary": "Change the registry's billing tier",
        "description": "Change your container registry's billing tier. Same tier as today is a no-op (200, nothing changes, no audit entry). An UPGRADE (new tier's quota ≥ current) is always allowed and effective immediately. A DOWNGRADE is allowed only when the account has never been measured yet, or its latest usage sample is at or under the NEW tier's quota, otherwise it is refused (409) so you can never downgrade out from under your own stored data. Requires scope `services:write`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RegistryTierChange"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Tier changed (or unchanged, if you asked for the tier you already have). Does NOT include usage/quota/push, re-read GET /registry for the refreshed policy.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/RegistryAccount"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid body: tier must be one of free, starter, premium, enterprise",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Registry is not enabled for this account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Either the container registry is closed (error.details[0].message == \"account_closed\"), or the new tier's quota is below the account's current measured usage (error.details[0].message == \"tier_below_usage\")",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The container registry is not launched yet (error.code == \"BACKEND_UNAVAILABLE\"). Every registry write answers this until launch (error.message == \"The container registry is not available yet.\"), and nothing is changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "delete": {
        "tags": [
          "Registry"
        ],
        "summary": "Close the container registry",
        "description": "Close your container registry. This does NOT delete your images immediately: the account stays reopenable with the SAME handle (POST /registry) until `deleteAfter`, after which an hourly job deletes its storage for good. While closed, no mutation is accepted (tier change, credential create/revoke, or a second close) except the reopen itself. Requires scope `services:write`.",
        "parameters": [
          {
            "name": "confirm",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Must equal this account's own handle, typed by the customer to confirm, same convention as DELETE /object-storage/buckets/{id}."
          }
        ],
        "responses": {
          "202": {
            "description": "Closed. Storage deletion is scheduled, not immediate.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/RegistryCloseResult"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "confirm is missing or does not equal the registry handle",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Registry is not enabled for this account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The container registry is already closed (error.details[0].message == \"account_closed\"), or the account is suspended and cannot be closed by its owner (error.details[0].message == \"account_suspended\"); contact support instead",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The container registry is not launched yet (error.code == \"BACKEND_UNAVAILABLE\"). Every registry write answers this until launch (error.message == \"The container registry is not available yet.\"), and nothing is changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/registry/tiers": {
      "get": {
        "tags": [
          "Registry"
        ],
        "summary": "List container registry tiers and their prices",
        "description": "The four billing tiers, priced in EUR and (when a live FX rate is available) USD. An UNAUTHENTICATED caller sees ONLY the tiers marked `available: true`: while the container registry stays catalog-hidden pre-launch, that is an empty list. A SIGNED-IN caller (cookie or bearer, no particular scope required, this endpoint has no per-caller data) sees all four tiers, `available` on each saying whether it is generally available yet, so the console's own tier picker can price them before the public catalog flips. A missing, expired or malformed credential is treated as anonymous rather than refused.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/RegistryTierInfo"
                          }
                        }
                      },
                      "required": [
                        "items"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {},
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/registry/handles/suggest": {
      "get": {
        "tags": [
          "Registry"
        ],
        "summary": "Suggest a free, valid container-registry handle",
        "description": "Up to 3 handle suggestions, derived from `?base=` (typically whatever the customer has typed so far), each always matching the handle shape (`^[a-z0-9]{3,30}$`), never a reserved word, and never already taken by another account. Omit `base` for unrelated suggestions. May return fewer than 3 in rare cases (never zero unless the namespace around your `base` is extremely crowded). Requires scope `services:read`.",
        "parameters": [
          {
            "name": "base",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Free-form text to base the suggestions on, e.g. what the customer has typed so far. Lowercased and stripped to the handle character class before use; omit for unrelated suggestions."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "suggestions": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "pattern": "^[a-z0-9]{3,30}$"
                          }
                        }
                      },
                      "required": [
                        "suggestions"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/registry/credentials": {
      "post": {
        "tags": [
          "Registry"
        ],
        "summary": "Create a registry credential",
        "description": "Create a robot credential for `docker login` / pull / push. The secret is returned exactly once, in this response, it is never shown again and never retrievable. Requires scope `services:write`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "pattern": "^[a-z0-9][a-z0-9-]{1,30}$",
                    "description": "Your label for the credential; also half of the login username, `<handle>+<name>`."
                  },
                  "scope": {
                    "type": "string",
                    "enum": [
                      "pull",
                      "push"
                    ]
                  },
                  "expiresAt": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Optional expiry. Omit for a credential that never expires."
                  }
                },
                "required": [
                  "name",
                  "scope"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Credential created, the secret is in this response only",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/RegistryCredentialCreated"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "400": {
            "description": "Invalid name, scope, or expiresAt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "The container registry is not enabled for this account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A credential with that name already exists, the account is closed (error.details[0].message == \"account_closed\"), or this account already holds the maximum number of non-revoked credentials (error.details[0].message == \"credential_limit\"; a revoked credential never counts against the limit). Also IDEMPOTENCY_IN_PROGRESS: a request with the same Idempotency-Key is still running; retry after Retry-After seconds with the same key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          },
          "503": {
            "description": "The container registry is not launched yet (error.code == \"BACKEND_UNAVAILABLE\"). Every registry write answers this until launch (error.message == \"The container registry is not available yet.\"), and nothing is changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      },
      "get": {
        "tags": [
          "Registry"
        ],
        "summary": "List registry credentials",
        "description": "List your registry credentials. Never includes a secret. Requires scope `services:read`.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/RegistryCredential"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "The container registry is not enabled for this account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/registry/credentials/{id}": {
      "delete": {
        "tags": [
          "Registry"
        ],
        "summary": "Revoke a registry credential",
        "description": "Revoke a registry credential immediately — it stops authenticating right away. Requires scope `services:write`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Credential id. Not a valid uuid → 400. Unknown, someone else's, or already revoked → 404."
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked — no content"
          },
          "400": {
            "description": "id is not a valid uuid",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "The container registry is not enabled for this account, or the credential is unknown / not yours / already revoked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The container registry is closed (error.details[0].message == \"account_closed\")",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The container registry is not launched yet (error.code == \"BACKEND_UNAVAILABLE\"). Every registry write answers this until launch (error.message == \"The container registry is not available yet.\"), and nothing is changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/registry/clusters": {
      "get": {
        "tags": [
          "Registry"
        ],
        "summary": "List clusters linked to your container registry",
        "description": "Every Kubernetes cluster you have linked to this registry account, joined against your owned clusters for a display name. A link whose cluster can no longer be found reports `status: \"error\"` with `lastError: \"cluster not found\"` rather than disappearing silently. Requires scope `services:read`.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/RegistryClusterLink"
                          }
                        }
                      },
                      "required": [
                        "items"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "The container registry is not enabled for this account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/registry/clusters/{serviceId}": {
      "post": {
        "tags": [
          "Registry"
        ],
        "summary": "Link a Kubernetes cluster to your container registry",
        "description": "Links one of your OWN Kubernetes clusters (ownership is checked server-side, a foreign or unknown serviceId is 404, never trusted from the URL alone) so every namespace in it, present and future, can pull from this registry without you copying a secret by hand. Mints a dedicated pull credential (`k8s-<serviceId>`), then installs a small controller (the RareCloud registry secret-syncer) in the cluster through the same Gardener kubeconfig bridge used for the rest of cluster management. A cluster that is not reachable yet (still provisioning) is not an error: the link is recorded as `status: \"linking\"` and the hourly reconciler finishes the install once the cluster becomes reachable, with no need to call this again. The full CR-7 disclosure text is returned verbatim in the `disclosure` field on success; see RegistryClusterLink and docs/registry/KUBERNETES.md for what the installed controller can read and write. Requires scope `services:write`; denied while impersonating.",
        "parameters": [
          {
            "name": "serviceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The cluster's service id (as returned by POST/GET /services)."
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "202": {
            "description": "Accepted. `link.status` is `\"active\"` when the install completed synchronously (the cluster was already reachable), or `\"linking\"` when it is queued for the reconciler.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "link": {
                          "type": "object",
                          "properties": {
                            "serviceId": {
                              "type": "string"
                            },
                            "status": {
                              "type": "string",
                              "enum": [
                                "linking",
                                "active",
                                "rotating",
                                "unlinking",
                                "error"
                              ]
                            },
                            "credentialName": {
                              "type": "string",
                              "description": "The pull credential minted for this cluster, e.g. `k8s-<serviceId>`. An empty string when the cluster is not Ready yet (still creating) and nothing has been minted this attempt."
                            },
                            "secretName": {
                              "type": "string",
                              "description": "The Kubernetes Secret name installed in the cluster (always `rarecloud-registry-credentials` today)."
                            },
                            "installs": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              },
                              "description": "The objects applied this call, e.g. `[\"Secret/rarecloud-registry-credentials\", \"ServiceAccount/rarecloud-registry-syncer\", ...]`. Empty when the install did not run this attempt (cluster not reachable yet)."
                            }
                          },
                          "required": [
                            "serviceId",
                            "status",
                            "credentialName",
                            "secretName",
                            "installs"
                          ]
                        },
                        "disclosure": {
                          "type": "string",
                          "description": "The CR-7 blast-radius disclosure text, verbatim, every time."
                        }
                      },
                      "required": [
                        "link",
                        "disclosure"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "404": {
            "description": "The container registry is not enabled for this account, or the cluster is unknown / not yours",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "This cluster is already linked (error.details[0].message == \"already_linked\"). Also IDEMPOTENCY_IN_PROGRESS: a request with the same Idempotency-Key is still running; retry after Retry-After seconds with the same key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          },
          "503": {
            "description": "The container registry is not launched yet (error.code == \"BACKEND_UNAVAILABLE\"). Every registry write answers this until launch (error.message == \"The container registry is not available yet.\"), and nothing is changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "delete": {
        "tags": [
          "Registry"
        ],
        "summary": "Unlink a Kubernetes cluster from your container registry",
        "description": "Removes everything linking installed from the cluster (the secret-syncer, every namespace's copy of the Secret, every patched ServiceAccount's imagePullSecrets entry, the RBAC objects) and revokes the pull credential. Unreachable at call time: the link stays `\"unlinking\"` and the hourly reconciler finishes the teardown once the cluster answers again. A cluster that no longer exists is treated as already clean in-cluster (there is nothing left to uninstall FROM) and is removed immediately. Calling this again on a cluster already mid-unlink is a no-op 202, not an error. Requires scope `services:write`; denied while impersonating.",
        "parameters": [
          {
            "name": "serviceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The cluster's service id."
          }
        ],
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "202": {
            "description": "Accepted (idempotent). `status` is `\"unlinked\"` once teardown finished synchronously, or `\"unlinking\"` while it is still in progress (including a repeat call).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "unlinking",
                            "unlinked"
                          ]
                        }
                      },
                      "required": [
                        "status"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "The container registry is not enabled for this account, or this cluster is not linked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The container registry is not launched yet (error.code == \"BACKEND_UNAVAILABLE\"). Every registry write answers this until launch (error.message == \"The container registry is not available yet.\"), and nothing is changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/registry/clusters/{serviceId}/rotate": {
      "post": {
        "tags": [
          "Registry"
        ],
        "summary": "Rotate a linked cluster's pull credential",
        "description": "Mints a fresh pull credential, pushes it into the cluster's source Secret, and polls (up to 30s) until every namespace's fanned-out copy of the Secret carries the new value before revoking the OLD credential, so the cluster never has a moment with no working credential. If not every copy has caught up by the time this call returns, the response is `status: \"rotating\"` and BOTH credentials stay live until the hourly reconciler confirms propagation and finishes the swap; a cluster that cannot be reached at all keeps both credentials live until the next reconcile. Requires scope `services:write`; denied while impersonating.",
        "parameters": [
          {
            "name": "serviceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The cluster's service id."
          }
        ],
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "202": {
            "description": "Accepted. `status` is `\"active\"` once the syncer confirmed readiness within the wait window, or `\"rotating\"` if it had not yet (the reconciler finishes it).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "active",
                            "rotating"
                          ]
                        },
                        "credentialName": {
                          "type": "string",
                          "description": "The newly-minted credential's name, e.g. `k8s-<serviceId>-<yyyymmddhhmm>`."
                        }
                      },
                      "required": [
                        "status",
                        "credentialName"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "The container registry is not enabled for this account, this cluster is not linked, or it is no longer owned by you",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A rotation is already in progress for this cluster (error.details[0].message == \"rotation_in_progress\")",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The cluster could not be reached to complete the rotation (error.message == \"cluster_unreachable\" or \"kubeconfig_unavailable\"). The OLD credential is untouched and still active; retry once the cluster is reachable. Also returned, with nothing changed, while the container registry is not launched yet (error.message == \"The container registry is not available yet.\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/registry/docker-credentials/kubernetes": {
      "post": {
        "tags": [
          "Registry"
        ],
        "summary": "Get a ready-to-apply pull-secret manifest for a cluster you manage yourself",
        "description": "For clusters RareCloud does not manage (BYO: your own kind/EKS/GKE/bare-metal cluster), the secret-syncer only installs where the api has direct cluster access, which a cluster we do not run never has. This CREATES a new pull (or push) credential every call, so the whole flow is one line: `kubectl apply -f <(curl -X POST ...)`. POST, not GET (M5, final fix wave): minting a credential is a state change, and the session cookie is `SameSite=lax`, which a cross-site top-level navigation can drive into a GET but not a POST. Query parameters are unchanged (still read from the URL, not a request body). Returns `Content-Type: application/yaml` by default (a plain Kubernetes Secret manifest, `type: kubernetes.io/dockerconfigjson`, no RareCloud labels, this is an UNMANAGED, customer-owned object, unlike the managed copies the secret-syncer installs) with a comment header explaining `kubectl apply` and the one-line ServiceAccount patch; send `Accept: application/json` to get the same credential and manifest back as structured JSON instead (used by the CLI/Terraform/MCP integrations, Plan 4). The secret is shown ONCE, in this response, whichever form you request. Counts against the account's credential cap (Plan 2). Requires scope `services:write`; denied while impersonating.",
        "parameters": [
          {
            "name": "scope",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "pull",
                "push"
              ],
              "default": "pull"
            }
          },
          {
            "name": "expiry",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "30d",
                "90d",
                "1y",
                "never"
              ],
              "default": "30d"
            }
          },
          {
            "name": "namespace",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "default"
            },
            "description": "The namespace the manifest targets. Must be a valid DNS-1123 label."
          },
          {
            "name": "secretName",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "rarecloud-registry"
            },
            "description": "The Secret's name in the manifest, and the name used in the suggested `kubectl patch serviceaccount` one-liner. Must be a valid DNS-1123 label."
          }
        ],
        "responses": {
          "201": {
            "description": "Created. The secret is shown ONCE, in this response only.",
            "content": {
              "application/yaml": {
                "schema": {
                  "type": "string",
                  "description": "A commented Kubernetes Secret manifest, ready for `kubectl apply -f`."
                }
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "credential": {
                          "type": "object",
                          "properties": {
                            "id": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "name": {
                              "type": "string",
                              "description": "Auto-generated, e.g. `byo-a1b2c3d4`."
                            },
                            "scope": {
                              "type": "string",
                              "enum": [
                                "pull",
                                "push"
                              ]
                            },
                            "username": {
                              "type": "string"
                            },
                            "expiresAt": {
                              "type": [
                                "string",
                                "null"
                              ],
                              "format": "date-time"
                            },
                            "createdAt": {
                              "type": "string",
                              "format": "date-time"
                            }
                          },
                          "required": [
                            "id",
                            "name",
                            "scope",
                            "username",
                            "expiresAt",
                            "createdAt"
                          ]
                        },
                        "manifest": {
                          "type": "string",
                          "description": "The exact same manifest text as the `application/yaml` response body."
                        },
                        "secret": {
                          "type": "string",
                          "description": "The credential secret. Shown exactly once, here; never retrievable again."
                        }
                      },
                      "required": [
                        "credential",
                        "manifest",
                        "secret"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid scope, expiry, namespace, or secretName (namespace/secretName must be valid DNS-1123 labels)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "The container registry is not enabled for this account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "This account already holds the maximum number of non-revoked registry credentials (error.details[0].message == \"credential_limit\"), or a random credential name could not be allocated after a retry (exceedingly rare)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The container registry is not launched yet (error.code == \"BACKEND_UNAVAILABLE\"). Every registry write answers this until launch (error.message == \"The container registry is not available yet.\"), and nothing is changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/registry/token": {
      "get": {
        "tags": [
          "Registry"
        ],
        "summary": "Docker Registry v2 token endpoint",
        "description": "Issues the RS256 bearer token a Docker/OCI client presents to the registry itself. Called automatically by `docker login` / `docker pull` / `docker push` — not something you call by hand. Authenticates with HTTP Basic ONLY: username `<handle>+<credential name>`, password = the credential secret (no bearerAuth/cookieAuth here). Cross-tenant scopes (a repository outside your own handle namespace) are silently dropped from the grant rather than rejected; the registry then denies access to them. Rate-limited: 30 requests/min per IP, and a credential is throttled after 10 authentication failures/min.",
        "parameters": [
          {
            "name": "service",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Must equal the registry's own hostname (the token audience). Any other value → 400."
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "style": "form",
            "explode": true,
            "description": "Repeatable. Each entry is `repository:<name>:<actions>`, e.g. `repository:acme/app:pull,push`. Omit to receive a token with no grants (identity check only)."
          }
        ],
        "responses": {
          "200": {
            "description": "Bearer token",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "no-store"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegistryToken"
                }
              }
            }
          },
          "400": {
            "description": "Unknown `service`, or a malformed `scope` entry",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegistryTokenError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid HTTP Basic credentials, or the credential is revoked/expired/on a closed account. Always the same generic message — the response never reveals which.",
            "headers": {
              "WWW-Authenticate": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "Basic realm=\"rarecloud-registry\""
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegistryTokenError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests, from this IP or against this credential",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegistryTokenError"
                }
              }
            }
          },
          "503": {
            "description": "The container registry is not launched yet: no token is issued (errors[0].code == \"UNAVAILABLE\"). Docker/OCI clients report it as a login or pull/push failure.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegistryTokenError"
                }
              }
            }
          }
        },
        "security": [
          {
            "basicAuth": []
          }
        ]
      },
      "post": {
        "tags": [
          "Registry"
        ],
        "summary": "Docker Registry v2 token endpoint (form-encoded)",
        "description": "Identical to GET /registry/token. Some Docker/OCI clients POST instead of GET; `service`/`scope` are read from the query string first and, only for a POST, from an `application/x-www-form-urlencoded` body when the query string omitted them.",
        "parameters": [
          {
            "name": "service",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Same as GET. Required overall (from the query string or the form body); must equal the registry's own hostname."
          },
          {
            "name": "scope",
            "in": "query",
            "required": false,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "style": "form",
            "explode": true,
            "description": "Same as GET. Repeatable `repository:<name>:<actions>`; may instead arrive form-encoded in the body."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "service": {
                    "type": "string"
                  },
                  "scope": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Bearer token",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "no-store"
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegistryToken"
                }
              }
            }
          },
          "400": {
            "description": "Unknown `service`, or a malformed `scope` entry",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegistryTokenError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid HTTP Basic credentials, or the credential is revoked/expired/on a closed account. Always the same generic message — the response never reveals which.",
            "headers": {
              "WWW-Authenticate": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "Basic realm=\"rarecloud-registry\""
                  ]
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegistryTokenError"
                }
              }
            }
          },
          "429": {
            "description": "Too many requests, from this IP or against this credential",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegistryTokenError"
                }
              }
            }
          },
          "503": {
            "description": "The container registry is not launched yet: no token is issued (errors[0].code == \"UNAVAILABLE\"). Docker/OCI clients report it as a login or pull/push failure.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegistryTokenError"
                }
              }
            }
          }
        },
        "security": [
          {
            "basicAuth": []
          }
        ]
      }
    },
    "/registry/repositories": {
      "get": {
        "tags": [
          "Registry"
        ],
        "summary": "List your registry repositories",
        "description": "List the repositories under your own handle. The `{repo}` shown here NEVER carries your handle, pass it straight to the endpoints below. Requires scope `services:read`.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            },
            "description": "Page size, clamped to 1..100."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor from a previous page's nextCursor. Missing, empty, or unreadable → the first page (never an error)."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/RegistryRepositorySummary"
                          }
                        },
                        "nextCursor": {
                          "type": "string",
                          "description": "Present only when another page follows."
                        }
                      },
                      "required": [
                        "items"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "The container registry is not enabled for this account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The registry backend is temporarily unavailable, try again",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/registry/repositories/{repo}": {
      "get": {
        "tags": [
          "Registry"
        ],
        "summary": "Get one repository and its tags",
        "description": "One repository's full tag list, each tag with its own CVE severity counts. `{repo}` MAY itself contain `/` (e.g. `team/app`), it is always resolved under your own handle, never anyone else's. Dispatch note: a `{repo}` value whose OWN name ends exactly like `tags/<tag>/vulnerabilities` is always read as a request for GET .../tags/{tag}/vulnerabilities instead of this endpoint, inherent to allowing `/` inside repository names (a repository literally named e.g. `acme/tags/build/vulnerabilities`-shaped cannot be addressed here; this never crosses into another tenant's namespace). Requires scope `services:read`.",
        "parameters": [
          {
            "name": "repo",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Repository path relative to your handle. May contain `/`."
          }
        ],
        "responses": {
          "200": {
            "description": "OK. An empty tags array means the repository does not exist, or exists but currently holds no images, both read the same way.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/RegistryRepository"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "The container registry is not enabled for this account, or {repo} is malformed / outside your own namespace. Never distinguishes the two, so a namespace probe learns nothing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The registry backend is temporarily unavailable, try again",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "delete": {
        "tags": [
          "Registry"
        ],
        "summary": "Delete an entire repository",
        "description": "Deletes every distinct image (manifest digest) in this repository, irreversible. `{repo}` MAY itself contain `/`; it is always resolved under your own handle. Dispatch note: `{repo}` values ending exactly like `tags/<tag>` or `manifests/<digest>` are always read as DELETE .../tags/{tag} or DELETE .../manifests/{digest} instead of this endpoint, inherent to allowing `/` inside repository names (a repository whose own name ends that way cannot be addressed by this endpoint; this never crosses into another tenant's namespace). Requires scope `services:write`.",
        "parameters": [
          {
            "name": "repo",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Repository path relative to your handle. May contain `/`."
          }
        ],
        "responses": {
          "202": {
            "description": "Deleted. `deleted` is the count of distinct manifest digests removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "deleted": {
                          "type": "integer"
                        }
                      },
                      "required": [
                        "deleted"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "The container registry is not enabled for this account, {repo} is malformed / outside your own namespace, or the repository has no tags at all (unlike GET, an empty repository 404s here rather than reporting nothing to delete)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The registry backend is temporarily unavailable, try again. Also returned, with nothing changed, while the container registry is not launched yet (error.message == \"The container registry is not available yet.\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/registry/repositories/{repo}/tags/{tag}/vulnerabilities": {
      "get": {
        "tags": [
          "Registry"
        ],
        "summary": "List CVE findings for one tag",
        "description": "Paginated vulnerability findings for one tag, from zot's Trivy scan. No separate \"tag not found\" signal, an unknown tag returns an empty items list, the same as a tag with no findings. Requires scope `services:read`.",
        "parameters": [
          {
            "name": "repo",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Repository path relative to your handle. May contain `/`."
          },
          {
            "name": "tag",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            },
            "description": "Page size, clamped to 1..100."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor from a previous page's nextCursor. Missing, empty, or unreadable → the first page (never an error)."
          }
        ],
        "responses": {
          "200": {
            "description": "OK. `unavailable: true` means zot's CVE/search extension is disabled, items is empty, not a real \"no findings\" answer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/RegistryCve"
                          }
                        },
                        "nextCursor": {
                          "type": "string",
                          "description": "Present only when another page follows."
                        },
                        "unavailable": {
                          "type": "boolean"
                        }
                      },
                      "required": [
                        "items",
                        "unavailable"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "The container registry is not enabled for this account, or {repo} is malformed / outside your own namespace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The registry backend is temporarily unavailable, try again",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/registry/repositories/{repo}/tags/{tag}": {
      "delete": {
        "tags": [
          "Registry"
        ],
        "summary": "Delete one tag",
        "description": "Removes ONLY this tag reference. The underlying manifest, and any OTHER tag still pointing at the same digest, is untouched. Requires scope `services:write`.",
        "parameters": [
          {
            "name": "repo",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Repository path relative to your handle. May contain `/`."
          },
          {
            "name": "tag",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted, no content"
          },
          "404": {
            "description": "The container registry is not enabled for this account, {repo} is malformed / outside your own namespace, or the tag does not exist",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The registry backend is temporarily unavailable, try again. Also returned, with nothing changed, while the container registry is not launched yet (error.message == \"The container registry is not available yet.\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/registry/repositories/{repo}/manifests/{digest}": {
      "delete": {
        "tags": [
          "Registry"
        ],
        "summary": "Delete a manifest by digest",
        "description": "Deletes the underlying image outright, every tag that pointed at this digest goes with it. If more than one tag currently serves this digest, the delete is refused (409) unless `confirm=true` is passed, since deleting a shared digest silently untags every one of them. Requires scope `services:write`.",
        "parameters": [
          {
            "name": "repo",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Repository path relative to your handle. May contain `/`."
          },
          {
            "name": "digest",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^sha256:[a-f0-9]{64}$"
            }
          },
          {
            "name": "confirm",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "true"
              ]
            },
            "description": "Pass confirm=true to delete a digest that is still referenced by more than one tag."
          }
        ],
        "responses": {
          "202": {
            "description": "Deleted. `tags` lists every tag that was serving this digest and is now untagged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "deleted": {
                          "type": "integer",
                          "enum": [
                            1
                          ]
                        },
                        "tags": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "deleted",
                        "tags"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "digest does not match sha256:<64 hex characters>",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "The container registry is not enabled for this account, {repo} is malformed / outside your own namespace, or no manifest with this digest exists in it",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "This digest is served by more than one tag and confirm=true was not passed. error.details has one {field:\"tags\", message:<tag>} entry per tag currently serving it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The registry backend is temporarily unavailable, try again. Also returned, with nothing changed, while the container registry is not launched yet (error.message == \"The container registry is not available yet.\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/object-storage": {
      "get": {
        "tags": [
          "Object Storage"
        ],
        "summary": "Get your object storage account",
        "description": "Your object storage account with its regions, price card, last measured usage and this month's accrued cost. Returns null if you have not enabled object storage. Requires scope `services:read`.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/ObjectStorageAccount"
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "The account, or null when object storage is not enabled."
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "post": {
        "tags": [
          "Object Storage"
        ],
        "summary": "Enable object storage",
        "description": "Enable object storage for your account and return it. Idempotent: calling it again returns the existing account, repairing it if preparation had stopped half-way. Requires scope `services:write`.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/ObjectStorageAccount"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      },
      "delete": {
        "tags": [
          "Object Storage"
        ],
        "summary": "Disable object storage",
        "description": "Remove your object storage account. Refused with CONFLICT while any bucket or active access key still exists — delete those first. Requires scope `services:write`.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        }
                      },
                      "required": [
                        "ok"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/object-storage/regions": {
      "get": {
        "tags": [
          "Object Storage"
        ],
        "summary": "List object storage regions",
        "description": "The regions you can create buckets in. Requires scope `services:read`.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Region id, e.g. eu-central-1."
                          },
                          "label": {
                            "type": "string",
                            "description": "The city the region is in."
                          },
                          "country": {
                            "type": "string",
                            "description": "ISO country code."
                          },
                          "eu": {
                            "type": "boolean",
                            "description": "Whether the region is inside the EU."
                          }
                        },
                        "required": [
                          "id",
                          "label",
                          "country",
                          "eu"
                        ]
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/object-storage/usage": {
      "get": {
        "tags": [
          "Object Storage"
        ],
        "summary": "Account usage series",
        "description": "Daily measured usage for the whole account: stored, deleted and egress bytes, plus the CDN traffic of every public bucket it hosts. A day the measurement did not run is absent — a gap, never a zero. Requires scope `services:read`.",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 90,
              "default": 30
            },
            "description": "How many days back to return, clamped to 1..90."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ObjectStorageUsagePoint"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/object-storage/buckets": {
      "get": {
        "tags": [
          "Object Storage"
        ],
        "summary": "List your buckets",
        "description": "Every bucket you own. An empty list — not a 404 — when object storage is not enabled. Requires scope `services:read`.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ObjectStorageBucket"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "post": {
        "tags": [
          "Object Storage"
        ],
        "summary": "Create a bucket",
        "description": "Create a bucket in one of the regions from GET /object-storage/regions. The name is 3-40 characters of lower-case letters, digits and hyphens, starting and ending with a letter or digit, and may not contain a dot. Requires scope `services:write`.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/ObjectStorageBucket"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 40,
                    "description": "Your bucket name. Unique within your account."
                  },
                  "region": {
                    "type": "string",
                    "description": "A region id from GET /object-storage/regions."
                  },
                  "versioning": {
                    "type": "boolean",
                    "default": false,
                    "description": "Keep object versions. Old versions are stored data and are billed."
                  },
                  "handle": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 16,
                    "pattern": "^[a-z][a-z0-9]{2,15}$",
                    "description": "Your namespace: the prefix every bucket of your account carries, so the bucket's full name (what an S3 client uses, and what `name` in the response holds) is `<handle>-<name>`. REQUIRED when your account has no namespace yet, which is the case for your first bucket; it is claimed with that bucket and can never be changed. On later buckets omit it or send exactly the stored value (see `handle` on GET /object-storage); a different value is refused. 3-16 lowercase letters and digits, starting with a letter, no hyphen. Some names are reserved. Errors: 400 INVALID_PARAM (missing on the first bucket, malformed or reserved, a different value from the stored one, or sent for an account that does not use a namespace), 409 CONFLICT (another account holds it)."
                  }
                },
                "required": [
                  "name",
                  "region"
                ]
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/object-storage/buckets/{id}": {
      "get": {
        "tags": [
          "Object Storage"
        ],
        "summary": "Get one bucket",
        "description": "One bucket by id. A bucket you do not own returns 404, indistinguishable from a missing one. Requires scope `services:read`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Bucket id. An id that is not yours returns 404, never 403."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/ObjectStorageBucket"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "patch": {
        "tags": [
          "Object Storage"
        ],
        "summary": "Update a bucket",
        "description": "Turn versioning or public delivery on or off. Only the fields you send are changed. A suspended account may still turn things OFF; it is refused only when it asks for more. Requires scope `services:write`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Bucket id. An id that is not yours returns 404, never 403."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/ObjectStorageBucket"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "versioning": {
                    "type": "boolean",
                    "description": "Keep object versions."
                  },
                  "public": {
                    "type": "boolean",
                    "description": "Deliver the bucket publicly over the CDN. Turning it on adds the per-bucket CDN monthly fee."
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Object Storage"
        ],
        "summary": "Delete a bucket",
        "description": "Delete a bucket. Without purge a bucket that still holds objects is refused with CONFLICT. Requires scope `services:write`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Bucket id. An id that is not yours returns 404, never 403."
          },
          {
            "name": "confirm",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Must equal the bucket's name."
          },
          {
            "name": "purge",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "Delete every object in the bucket first. ONLY the literal string \"true\" enables it — any other value, including \"1\" or \"yes\", leaves it off. Deleted storage is still billed for up to 90 days."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        }
                      },
                      "required": [
                        "ok"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/object-storage/buckets/{id}/usage": {
      "get": {
        "tags": [
          "Object Storage"
        ],
        "summary": "Bucket usage series",
        "description": "Daily measured usage for one bucket, plus its CDN traffic when it is public. egressBytes is always 0 here: egress is measured per account, so GET /object-storage/usage is the authority for it. Requires scope `services:read`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Bucket id. An id that is not yours returns 404, never 403."
          },
          {
            "name": "days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 90,
              "default": 30
            },
            "description": "How many days back to return, clamped to 1..90."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ObjectStorageUsagePoint"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/object-storage/keys": {
      "get": {
        "tags": [
          "Object Storage"
        ],
        "summary": "List your access keys",
        "description": "Every access key on your account, active and revoked. Never carries a secret. Requires scope `services:read`.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ObjectStorageKey"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "post": {
        "tags": [
          "Object Storage"
        ],
        "summary": "Create an access key",
        "description": "Mint an S3 access key scoped to every bucket or to an explicit list. The response is the ONLY place secretAccessKey ever appears — it is not stored and cannot be retrieved again. Refused inside an impersonated support session. Requires scope `services:write`.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/ObjectStorageKeyCreated"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80,
                    "description": "Your label for the key."
                  },
                  "scope": {
                    "type": "object",
                    "properties": {
                      "buckets": {
                        "description": "\"*\" for every bucket in the account, or an explicit list of bucket ids.",
                        "oneOf": [
                          {
                            "type": "string",
                            "enum": [
                              "*"
                            ]
                          },
                          {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "format": "uuid"
                            },
                            "minItems": 1
                          }
                        ]
                      },
                      "access": {
                        "type": "string",
                        "enum": [
                          "read",
                          "readwrite"
                        ]
                      }
                    },
                    "required": [
                      "buckets",
                      "access"
                    ]
                  }
                },
                "required": [
                  "name",
                  "scope"
                ]
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/object-storage/keys/{id}": {
      "delete": {
        "tags": [
          "Object Storage"
        ],
        "summary": "Revoke an access key",
        "description": "Revoke an access key: the credential stops working immediately. Idempotent — revoking an already-revoked key succeeds. Requires scope `services:write`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Access key id. An id that is not yours returns 404, never 403."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        }
                      },
                      "required": [
                        "ok"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/sandboxes": {
      "get": {
        "tags": [
          "Sandboxes"
        ],
        "summary": "List sandboxes",
        "description": "Every sandbox the authenticated user owns. Tenant-scoped: you only ever see your own. Requires scope services:read.",
        "responses": {
          "503": {
            "$ref": "#/components/responses/BackendUnavailable"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "sandboxes": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Sandbox"
                          }
                        }
                      },
                      "required": [
                        "sandboxes"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Sandboxes"
        ],
        "summary": "Create a sandbox",
        "description": "Create an isolated code-execution sandbox from a template (default python). Billed per second of runtime. Requires scope services:write.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "template": {
                    "type": "string",
                    "enum": [
                      "python",
                      "node",
                      "base"
                    ],
                    "default": "python",
                    "description": "Sandbox template: python (data science), node, or base (shell only)."
                  },
                  "image": {
                    "type": "string",
                    "maxLength": 512,
                    "description": "Custom public container image (e.g. \"nginx:1.27\" or \"ghcr.io/acme/app:tag\"). Overrides the template's image — bring-your-own-image. Public registries only for now (no pull secrets). The template still drives pricing."
                  },
                  "timeoutMs": {
                    "type": "integer",
                    "minimum": 1000,
                    "maximum": 3600000,
                    "default": 300000,
                    "description": "Idle timeout in milliseconds (default 5 minutes, max 1 hour)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "503": {
            "$ref": "#/components/responses/BackendUnavailable"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Sandbox"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/sandboxes/{id}": {
      "get": {
        "tags": [
          "Sandboxes"
        ],
        "summary": "Get a sandbox",
        "description": "One sandbox by id. A sandbox you do not own returns 404 (indistinguishable from a missing one). Requires scope services:read.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Sandbox id."
          }
        ],
        "responses": {
          "503": {
            "$ref": "#/components/responses/BackendUnavailable"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Sandbox"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Sandboxes"
        ],
        "summary": "Delete a sandbox",
        "description": "Terminate and remove a sandbox. Requires scope services:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Sandbox id."
          }
        ],
        "responses": {
          "503": {
            "$ref": "#/components/responses/BackendUnavailable"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        }
                      },
                      "required": [
                        "ok"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/sandboxes/{id}/run_code": {
      "post": {
        "tags": [
          "Sandboxes"
        ],
        "summary": "Run code in a sandbox",
        "description": "Execute a snippet inside the sandbox (python3 -c / node -e) and return stdout, stderr and the exit code. Requires scope services:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Sandbox id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "language": {
                    "type": "string",
                    "enum": [
                      "python",
                      "node"
                    ]
                  },
                  "code": {
                    "type": "string",
                    "maxLength": 100000
                  }
                },
                "required": [
                  "language",
                  "code"
                ]
              }
            }
          }
        },
        "responses": {
          "503": {
            "$ref": "#/components/responses/BackendUnavailable"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/SandboxExecResult"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/sandboxes/{id}/commands": {
      "post": {
        "tags": [
          "Sandboxes"
        ],
        "summary": "Run a shell command in a sandbox",
        "description": "Execute a shell command (sh -c) inside the sandbox and return stdout, stderr and the exit code. Requires scope services:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Sandbox id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "cmd": {
                    "type": "string",
                    "maxLength": 10000
                  }
                },
                "required": [
                  "cmd"
                ]
              }
            }
          }
        },
        "responses": {
          "503": {
            "$ref": "#/components/responses/BackendUnavailable"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/SandboxExecResult"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/sandboxes/{id}/files": {
      "get": {
        "tags": [
          "Sandboxes"
        ],
        "summary": "Read a file from a sandbox",
        "description": "Read one file from the sandbox filesystem by absolute path. Requires scope services:read.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Sandbox id."
          },
          {
            "name": "path",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Absolute file path inside the sandbox."
          }
        ],
        "responses": {
          "503": {
            "$ref": "#/components/responses/BackendUnavailable"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "path": {
                          "type": "string"
                        },
                        "content": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "path",
                        "content"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Sandboxes"
        ],
        "summary": "Write a file into a sandbox",
        "description": "Create or overwrite one file in the sandbox filesystem (parent directories are created). Requires scope services:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Sandbox id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "path": {
                    "type": "string",
                    "description": "Absolute file path inside the sandbox."
                  },
                  "content": {
                    "type": "string",
                    "maxLength": 1000000
                  }
                },
                "required": [
                  "path",
                  "content"
                ]
              }
            }
          }
        },
        "responses": {
          "503": {
            "$ref": "#/components/responses/BackendUnavailable"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        }
                      },
                      "required": [
                        "ok"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/sandboxes/{id}/ports/{port}": {
      "post": {
        "tags": [
          "Sandboxes"
        ],
        "summary": "Expose a sandbox port",
        "description": "Expose a TCP port the sandbox is listening on and return its public preview URL. Requires scope services:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Sandbox id."
          },
          {
            "name": "port",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 65535
            },
            "description": "TCP port inside the sandbox."
          }
        ],
        "responses": {
          "503": {
            "$ref": "#/components/responses/BackendUnavailable"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "url": {
                          "type": "string",
                          "format": "uri"
                        }
                      },
                      "required": [
                        "url"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/account": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Get current account profile",
        "description": "Requires scope `account:read`.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/Account"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "patch": {
        "tags": [
          "Account"
        ],
        "summary": "Update account profile",
        "description": "Requires scope `account:write`. Pass only the fields to change. `email` updates the billing/contact email of the client record; the login email is not changed by this endpoint (contact support to change it).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Account"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/account/clients": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "List users linked to this client account",
        "description": "Requires scope `account:read`. Returns accepted users AND pending invitations. Each entry has a `status`: `active` (accepted user), `invited` (invitation sent, not yet accepted), or `disabled`. The account owner is flagged with `isOwner:true`.",
        "responses": {
          "200": {
            "description": "Success"
          }
        }
      },
      "post": {
        "tags": [
          "Account"
        ],
        "summary": "Invite a sub-user via email",
        "description": "Requires scope `account:write`. Sends the WHMCS invitation email (owner-attributed, with a token accept-link) and creates a pending invitation. Denied while impersonating a client. `permissions` accepts the buckets `manage_services`, `manage_billing`, `view_invoices`, `manage_tickets`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "permissions": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                },
                "required": [
                  "email"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Invitation sent",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/account/clients/resend": {
      "post": {
        "tags": [
          "Account"
        ],
        "summary": "Resend a pending sub-user invitation email",
        "description": "Requires scope `account:write`. Re-fires the invitation email for a pending (not-yet-accepted) invite identified by `email`. Denied while impersonating a client. Returns `NOT_IMPLEMENTED` when the invite bridge is not available, or `NOT_FOUND` when no pending invitation matches.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                },
                "required": [
                  "email"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Invitation re-sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "sent": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/account/affiliate": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Get your affiliate status and stats",
        "description": "Requires scope `account:read`. Owner-scoped: always the authenticated client's own affiliate record. Returns `{active:false}` when the affiliate program has not been activated for this account; otherwise the full native affiliate page: referral link, clicks (visitors) / signups / conversion rate, the commissions summary (pending maturation / available balance / total withdrawn), the payout minimum, and the per-referral list. Commission configuration is managed by RareCloud, not through this API.",
        "responses": {
          "200": {
            "description": "Affiliate state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "active": {
                          "type": "boolean"
                        },
                        "id": {
                          "type": "integer",
                          "description": "Affiliate id (only when active). The `aff` value in the referral link."
                        },
                        "referralLink": {
                          "type": "string",
                          "description": "Share this URL. Visits set a 90-day tracking cookie and count as visitors (only when active)."
                        },
                        "balanceCents": {
                          "type": "integer",
                          "description": "Available commissions balance in cents of `currency` (only when active)."
                        },
                        "pendingCents": {
                          "type": "integer",
                          "description": "Commissions pending maturation, in cents of `currency` (active + read-bridge deployed; omitted in the summary fallback)."
                        },
                        "withdrawnCents": {
                          "type": "integer",
                          "description": "Total already paid out, in cents of `currency` (only when active)."
                        },
                        "payoutMinimumCents": {
                          "type": "integer",
                          "description": "Minimum available balance required to request a withdrawal, in cents of `currency` (only when active)."
                        },
                        "visitors": {
                          "type": "integer",
                          "description": "Clicks: unique referral-link visits recorded (only when active)."
                        },
                        "signups": {
                          "type": "integer",
                          "description": "Referred sign-ups (active + read-bridge deployed; omitted in the summary fallback)."
                        },
                        "conversionRate": {
                          "type": "number",
                          "description": "Signups ÷ clicks × 100 (active + read-bridge deployed; omitted in the summary fallback)."
                        },
                        "since": {
                          "type": "string",
                          "description": "Affiliate activation date, YYYY-MM-DD (only in the summary fallback)."
                        },
                        "currency": {
                          "type": "string",
                          "description": "The account's billing currency code, e.g. EUR (only when active)."
                        },
                        "referrals": {
                          "type": "array",
                          "description": "Your referrals (only when active; empty in the summary fallback). One row per referred service.",
                          "items": {
                            "type": "object",
                            "properties": {
                              "date": {
                                "type": "string",
                                "description": "Signup / first-commission date, YYYY-MM-DD (empty when unknown)."
                              },
                              "service": {
                                "type": "string",
                                "description": "Product/Service label, e.g. \"KVM VPS - host.example.com\"."
                              },
                              "amountCents": {
                                "type": "integer",
                                "description": "The referred service's recurring amount, in cents of `currency`."
                              },
                              "commissionCents": {
                                "type": "integer",
                                "description": "Commission earned on this referral, in cents of `currency`."
                              },
                              "status": {
                                "type": "string",
                                "description": "Native status label, e.g. Pending / Confirmed."
                              }
                            }
                          }
                        }
                      },
                      "required": [
                        "active"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/account/affiliate/activate": {
      "post": {
        "tags": [
          "Account"
        ],
        "summary": "Activate the affiliate program for your account",
        "description": "Requires scope `account:write`. Owner-scoped and idempotent: activating an already-active affiliate account is a no-op that returns the current state. Returns the same shape as `GET /account/affiliate` with `active: true`.",
        "responses": {
          "200": {
            "description": "Affiliate activated (or already active); current affiliate state.",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/account/password": {
      "post": {
        "tags": [
          "Account"
        ],
        "summary": "Change account password",
        "description": "Requires scope `account:write`. Re-validates the current password first.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "currentPassword": {
                    "type": "string",
                    "maxLength": 256
                  },
                  "newPassword": {
                    "type": "string",
                    "minLength": 8,
                    "maxLength": 128
                  }
                },
                "required": [
                  "currentPassword",
                  "newPassword"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password changed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/account/two-factor": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Get two-factor (TOTP) status",
        "description": "Requires scope `account:read`. Returns the authenticated user's own 2FA status.",
        "responses": {
          "200": {
            "description": "2FA status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        },
                        "pendingSetup": {
                          "type": "boolean"
                        },
                        "backupCodesLeft": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "tags": [
          "Account"
        ],
        "summary": "Set up, enable, or disable two-factor (TOTP)",
        "description": "Requires scope `account:write`. Mutates the authenticated user's own 2FA. `action:setup` returns a secret + otpauth URL + QR data URL; `action:enable` (with a current code) turns 2FA on and returns one-time backup codes; `action:disable` (with a current code) turns it off.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "setup",
                      "enable",
                      "disable"
                    ]
                  },
                  "code": {
                    "type": "string",
                    "minLength": 6,
                    "maxLength": 16,
                    "description": "A current TOTP or backup code. Required for enable/disable (6-16 chars)."
                  }
                },
                "required": [
                  "action"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Result depends on the action: setup → {secret, otpauthUrl, qrDataUrl}; enable → {enabled:true, backupCodes}; disable → {disabled:true}.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid action or code",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/account/verify-email/resend": {
      "post": {
        "tags": [
          "Account"
        ],
        "summary": "Resend the email-address verification email",
        "description": "Requires scope `account:write`. Owner-scoped to the authenticated user — the route takes NO client id (the WHMCS client id is resolved server-side from the auth context), so there is no IDOR/existence-oracle surface. Already-verified accounts short-circuit without sending. Backed by WHMCS SendEmail against the \"Email Address Verification\" template.",
        "responses": {
          "200": {
            "description": "Resend outcome. `sent:true` when the email was dispatched; `sent:false` with `reason:\"already_verified\"` when the account email is already verified.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "sent": {
                          "type": "boolean"
                        },
                        "reason": {
                          "type": "string",
                          "description": "Present when sent=false, e.g. \"already_verified\"."
                        }
                      },
                      "required": [
                        "sent"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/account/ssh-keys": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "List account SSH keys",
        "description": "Requires scope `account:read`. Returns the authenticated user's own registered SSH public keys.",
        "responses": {
          "200": {
            "description": "The account's SSH keys",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SshKey"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "post": {
        "tags": [
          "Account"
        ],
        "summary": "Register an account SSH key",
        "description": "Requires scope `account:write`. Stores an OpenSSH public key on the account. The public key is validated and its SHA256 fingerprint is computed server-side. The key name must be unique per account (duplicate → 409 CONFLICT); an invalid public key → 400 INVALID_PARAM.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Display name for the key (unique per account)."
                  },
                  "publicKey": {
                    "type": "string",
                    "maxLength": 4096,
                    "description": "OpenSSH public key string (ssh-ed25519 / ssh-rsa / ecdsa-sha2-nistp*)."
                  }
                },
                "required": [
                  "name",
                  "publicKey"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created key (id, name, server-computed fingerprint)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "fingerprint": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "id",
                        "name",
                        "fingerprint"
                      ]
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "400": {
            "description": "Invalid public key or missing fields",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "A key with that name already exists. Also IDEMPOTENCY_IN_PROGRESS: a request with the same Idempotency-Key is still running; retry after Retry-After seconds with the same key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/account/ssh-keys/{id}": {
      "delete": {
        "tags": [
          "Account"
        ],
        "summary": "Delete an account SSH key",
        "description": "Requires scope `account:write`. Removes one of the authenticated user's own SSH keys. A key that is not yours → 404 NOT_FOUND (owner-scoped, IDOR-safe). Does NOT touch authorized_keys on already-provisioned servers.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/billing/credit": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "Get credit balance",
        "description": "Requires scope `billing:read`.",
        "responses": {
          "200": {
            "description": "Balance",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/CreditBalance"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/billing/invoices": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "List invoices",
        "description": "Requires scope `billing:read`.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "unpaid",
                "paid",
                "cancelled",
                "refunded",
                "collections"
              ]
            }
          },
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "sale",
                "deposit"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Invoice list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Invoice"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/billing/invoices/{id}": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "Get invoice detail",
        "description": "Requires scope `billing:read`. Cross-user access returns 404.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Invoice",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Invoice"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/billing/invoices/{id}/pdf": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "Download invoice PDF",
        "description": "Requires scope `billing:read`. Ownership-gated. WHMCS has no working API action to fetch an invoice PDF (GetInvoicePDF is not a valid action), so this mints a single-use SSO token and 302-redirects the customer to the WHMCS client-area invoice page (with the Download PDF button) where they are already logged in.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect to the WHMCS client-area invoice page (single-use SSO)."
          }
        }
      }
    },
    "/tickets": {
      "get": {
        "tags": [
          "Tickets"
        ],
        "summary": "List tickets",
        "description": "Requires scope `tickets:read`.",
        "responses": {
          "200": {
            "description": "Tickets"
          }
        }
      },
      "post": {
        "tags": [
          "Tickets"
        ],
        "summary": "Open a new ticket",
        "description": "Requires scope `tickets:write`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "subject": {
                    "type": "string",
                    "maxLength": 150
                  },
                  "department": {
                    "type": "string",
                    "maxLength": 64
                  },
                  "priority": {
                    "type": "string",
                    "enum": [
                      "low",
                      "medium",
                      "high"
                    ]
                  },
                  "body": {
                    "type": "string",
                    "maxLength": 50000
                  },
                  "attachments": {
                    "type": "array",
                    "maxItems": 5,
                    "description": "Optional file uploads (max 5). Each item is {name, data} where data is base64-encoded file content (no data: URI prefix).",
                    "items": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string",
                          "maxLength": 255,
                          "description": "Cannot contain path separators or null bytes."
                        },
                        "data": {
                          "type": "string",
                          "maxLength": 7000000,
                          "description": "Base64-encoded file content (max 7,000,000 chars, ~5.2 MB decoded)."
                        }
                      },
                      "required": [
                        "name",
                        "data"
                      ]
                    }
                  }
                },
                "required": [
                  "subject",
                  "department",
                  "priority",
                  "body"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/tickets/departments": {
      "get": {
        "tags": [
          "Tickets"
        ],
        "summary": "List support departments",
        "description": "Requires scope `tickets:read`. The numeric department ids accepted by `POST /tickets`.",
        "responses": {
          "200": {
            "description": "Departments",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/tickets/{id}": {
      "get": {
        "tags": [
          "Tickets"
        ],
        "summary": "Get ticket thread",
        "description": "Requires scope `tickets:read`. Cross-user access returns 404.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ticket"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/tickets/{id}/replies": {
      "post": {
        "tags": [
          "Tickets"
        ],
        "summary": "Reply to a ticket",
        "description": "Requires scope `tickets:write`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "body": {
                    "type": "string",
                    "maxLength": 50000
                  },
                  "attachments": {
                    "type": "array",
                    "maxItems": 5,
                    "description": "Optional file uploads (max 5). Each item is {name, data} where data is base64-encoded file content (no data: URI prefix).",
                    "items": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string",
                          "maxLength": 255,
                          "description": "Cannot contain path separators or null bytes."
                        },
                        "data": {
                          "type": "string",
                          "maxLength": 7000000,
                          "description": "Base64-encoded file content (max 7,000,000 chars, ~5.2 MB decoded)."
                        }
                      },
                      "required": [
                        "name",
                        "data"
                      ]
                    }
                  }
                },
                "required": [
                  "body"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reply posted",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        }
      }
    },
    "/proxies": {
      "get": {
        "tags": [
          "Proxies"
        ],
        "summary": "List your proxy services",
        "description": "Residential proxy services owned by the authenticated user. Each mobile line carries `port` (integer|null): the cached vendor port, so a specific line can be identified at a glance without opening its detail; null when not yet cached.",
        "responses": {
          "200": {
            "description": "Your residential proxy services. Each proxy also carries `apiAccess` (`full` | `read_only`): when `read_only`, API tokens cannot change the proxy or read its endpoints, credentials, auth settings or VPN config (403 `RESOURCE_PROTECTED`)."
          }
        }
      },
      "post": {
        "tags": [
          "Proxies"
        ],
        "summary": "Order a residential or mobile proxy plan (ISP, GB, or mobile)",
        "description": "Creates the service, issues a WHMCS invoice in your account currency, settles it from bonus+credit, and provisions when paid. Discriminated on `kind`: omit it (or send `residential-isp`) for a US Residential ISP plan; send `residential-gb` for a monthly GB Residential bucket (country + rotation are configured afterwards via proxy-requests); send `mobile-4g`, `mobile-vpn`, or `mobile-multisim` for TrumpProxies mobile lines.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "title": "US Residential ISP",
                    "type": "object",
                    "required": [
                      "ips",
                      "cycle",
                      "locationId",
                      "protocol",
                      "authType"
                    ],
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "residential-isp"
                        ],
                        "description": "Optional; defaults to residential-isp for backward-compatibility."
                      },
                      "ips": {
                        "type": "integer",
                        "description": "IP-count tier (1, 3, 5, 10, 20, 25, 50, 100, 150, 200)"
                      },
                      "cycle": {
                        "type": "string",
                        "enum": [
                          "day",
                          "week",
                          "month"
                        ]
                      },
                      "locationId": {
                        "type": "string",
                        "description": "One of the catalog locations"
                      },
                      "protocol": {
                        "type": "string",
                        "enum": [
                          "http",
                          "socks"
                        ]
                      },
                      "authType": {
                        "type": "string",
                        "enum": [
                          "password",
                          "combined"
                        ]
                      },
                      "replacements": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 2,
                        "description": "Proxy (ISP only): number of paid extra IP replacements to buy with the order (0–2)."
                      }
                    }
                  },
                  {
                    "title": "GB Residential",
                    "type": "object",
                    "required": [
                      "kind",
                      "gb"
                    ],
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "residential-gb"
                        ]
                      },
                      "gb": {
                        "type": "integer",
                        "description": "GB-bucket tier (1, 2, 5, 10, 50, 100, 250, 500, 1000); monthly only"
                      },
                      "payg": {
                        "type": "boolean",
                        "description": "Pay-as-you-go: bill this GB order at the per-GB rate instead of a fixed monthly bucket."
                      }
                    }
                  },
                  {
                    "title": "Mobile Proxy (4G/VPN/MultiSIM)",
                    "type": "object",
                    "required": [
                      "kind",
                      "quantity"
                    ],
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "mobile-4g",
                          "mobile-vpn",
                          "mobile-multisim"
                        ],
                        "description": "Mobile proxy type: 4G (residential), VPN (anonymity), or MultiSIM (multi-line)"
                      },
                      "quantity": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 10,
                        "description": "Number of mobile lines to order (1–10)"
                      },
                      "location": {
                        "type": "string",
                        "enum": [
                          "us",
                          "de",
                          "at"
                        ],
                        "default": "us",
                        "description": "Line location. Defaults to 'us' when omitted."
                      },
                      "months": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 12,
                        "default": 1,
                        "description": "Prepaid term in months (1, 3, 6, 12). A new order bills the plain monthly × N — no prepaid-term discount (the price already carries the initial promo); the 5/10/15% term discount applies only at renewal. Charged upfront."
                      },
                      "term": {
                        "type": "string",
                        "enum": [
                          "test24h"
                        ],
                        "description": "Order a 1-Day product test instead of a monthly term. Mutually exclusive with `months`; quantity is forced to 1. The line can later be upgraded to a full 1/3/6/12-month plan in place via POST /proxies/{id}/renew, keeping the same credentials."
                      },
                      "carrier": {
                        "type": "string",
                        "enum": [
                          "att",
                          "tmobile"
                        ],
                        "description": "Optional US carrier preference (US lines only). Omit for best available."
                      },
                      "platform": {
                        "type": "string",
                        "enum": [
                          "instagram",
                          "twitter",
                          "reddit",
                          "tiktok",
                          "facebook",
                          "linkedin",
                          "pinterest",
                          "bluesky",
                          "other"
                        ],
                        "description": "Optional target platform hint (routes the line to the strongest fit). Every platform works regardless."
                      },
                      "protocol": {
                        "type": "string",
                        "enum": [
                          "openvpn",
                          "wireguard"
                        ],
                        "description": "VPN protocol (mobile-vpn only): OpenVPN (broad compatibility) or WireGuard (faster). Defaults to OpenVPN."
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Order result: service, invoiceId, paid, proxies (when provisioned)",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/proxies/catalog": {
      "get": {
        "tags": [
          "Proxies"
        ],
        "summary": "Residential proxy catalog (tiers, locations, options)",
        "description": "ISP IP-count tiers with EUR + USD pricing per Day/Week/Month, orderable locations, and protocol + authentication options for the order wizard, plus a `gb` section of monthly GB Residential bucket tiers (EUR + USD) and pay-as-you-go pricing. Each ISP tier may include `replacementOptions` array with `{count, eur, usd}` entries for paid extra IP replacement packages.",
        "responses": {
          "200": {
            "description": "Catalog: tiers, locations, protocols, authTypes, gb, replacementOptions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [true]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "kind": {
                          "type": "string",
                          "enum": ["residential-isp"]
                        },
                        "cycles": {
                          "type": "array",
                          "items": { "type": "string" }
                        },
                        "locations": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": { "type": "string" },
                              "country": { "type": "string" },
                              "label": { "type": "string" }
                            }
                          }
                        },
                        "protocols": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": { "type": "string" },
                              "label": { "type": "string" }
                            }
                          }
                        },
                        "authTypes": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": { "type": "string" },
                              "label": { "type": "string" }
                            }
                          }
                        },
                        "tiers": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "ips": { "type": "integer" },
                              "eur": { "type": "object", "properties": { "day": { "type": "number" }, "week": { "type": "number" }, "month": { "type": "number" } } },
                              "usd": { "type": "object", "properties": { "day": { "type": "number" }, "week": { "type": "number" }, "month": { "type": "number" } } },
                              "replacementOptions": {
                                "type": "array",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "count": { "type": "integer" },
                                    "eur": { "type": "number" },
                                    "usd": { "type": "number" }
                                  }
                                }
                              }
                            }
                          }
                        },
                        "gb": {
                          "type": "object",
                          "properties": {
                            "cycle": {
                              "type": "string",
                              "enum": ["month"]
                            },
                            "tiers": {
                              "type": "array",
                              "items": {
                                "type": "object",
                                "properties": {
                                  "gb": { "type": "integer" },
                                  "eur": { "type": "number" },
                                  "usd": { "type": "number" }
                                }
                              }
                            },
                            "payg": {
                              "type": "object",
                              "properties": {
                                "eurPerGb": { "type": "number" },
                                "usdPerGb": { "type": "number" }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/proxies/mobile/prices": {
      "get": {
        "tags": [
          "Proxies"
        ],
        "summary": "Mobile proxy order prices",
        "description": "What ordering a mobile proxy (`POST /proxies` with kind `mobile-4g`, `mobile-vpn` or `mobile-multisim`) bills per line, for every kind × location, in your billing currency. A full plan bills `monthlyCents × months × quantity` (no prepaid-term discount on a new order); a 1-Day test (`term: test24h`) bills `oneDayCents` for its single line. Amounts are whole cents and include any fixed reseller price that applies to your account. `monthlyCents` is null when that product cannot be priced right now, and ordering it is refused until it can.",
        "responses": {
          "200": {
            "description": "Prices per kind × location",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "enum": [true] },
                    "data": {
                      "type": "object",
                      "properties": {
                        "currency": { "type": "string", "enum": ["EUR", "USD"] },
                        "prices": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "kind": { "type": "string", "enum": ["mobile-4g", "mobile-vpn", "mobile-multisim"] },
                              "location": { "type": "string", "enum": ["us", "de", "at"] },
                              "monthlyCents": { "type": ["integer", "null"] },
                              "oneDayCents": { "type": "integer" }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/proxies/{id}": {
      "get": {
        "tags": [
          "Proxies"
        ],
        "summary": "Get a proxy service",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Proxy service. Includes `reactivatable` (boolean) and `reactivationDaysLeft` (integer|null, mobile proxies only; a lapsed ISP/GB residential service is not reactivatable): a lapsed mobile service can still be renewed — reactivating the same line — for 14 days after expiry; `reactivationDaysLeft` counts 14…1 inside that window and is null when it does not apply (active, PAYG, 1-Day test, non-mobile kind, or past the window). Also includes `port` (integer|null): the cached vendor port for mobile lines, null when not yet cached. Each proxy also carries `apiAccess` (`full` | `read_only`): when `read_only`, API tokens cannot change the proxy or read its endpoints, credentials, auth settings or VPN config (403 `RESOURCE_PROTECTED`)."
          },
          "404": {
            "description": "Not found or not owned"
          }
        }
      }
    },
    "/proxies/{id}/proxy-list": {
      "get": {
        "tags": [
          "Proxies"
        ],
        "summary": "Proxy endpoints and credentials",
        "description": "The live proxy list (ip, port, username, password) for an active service. For a static residential (ISP) service an entry may additionally carry `location` \u2014 where THAT IP exits, as \"City (Network)\" \u2014 learned by an earlier live check (POST /proxies/{id}/check) and stored, so it is correct on first load. The field is omitted when nothing is known for that IP, and is never present for GB plans, whose endpoints are a rotating pool.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "Proxy endpoints with credentials",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "proxies": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "ip": {
                                "type": "string"
                              },
                              "port": {
                                "type": "integer"
                              },
                              "username": {
                                "type": "string"
                              },
                              "password": {
                                "type": "string"
                              },
                              "location": {
                                "type": "string",
                                "description": "Where this IP exits, as \"City (Network)\" (either half alone when only one is known). Static ISP services only, and only once a live check has learned it \u2014 omitted otherwise."
                              },
                              "rotationUrl": {
                                "type": "string",
                                "description": "Mobile VPN/4G lines only: the branded rotation link (these entries carry no ip/port)."
                              }
                            }
                          }
                        }
                      },
                      "required": [
                        "proxies"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/proxies/{id}/bandwidth": {
      "get": {
        "tags": [
          "Proxies"
        ],
        "summary": "Bandwidth usage (GB plans)",
        "description": "Monthly bandwidth usage for a metered GB residential plan. GB plans only; other kinds return INVALID_PARAM.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Bandwidth totals",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "unit": {
                      "type": "string",
                      "example": "MB"
                    },
                    "total": {
                      "type": "number"
                    },
                    "used": {
                      "type": "number"
                    },
                    "available": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/proxies/{id}/renew": {
      "post": {
        "tags": [
          "Proxies"
        ],
        "summary": "Renew a proxy service",
        "description": "Issues a renewal invoice for the stored cycle; on payment, prolongs the service. For a mobile 1-Day test line this UPGRADES it in place to the selected full term (1/3/6/12 months): it is charged the full-plan price (monthly × months × the 5/10/15 prepaid discount), keeps the same credentials, and becomes a normal full plan (the test payment is not credited). A lapsed mobile service can still be renewed — reactivating the same line, with the new expiry counted from now — for 14 days after expiry (mobile proxies only); a lapsed ISP/GB residential service cannot be renewed; order a new service. Past the window or if not mobile, the request is refused with 409 REACTIVATION_WINDOW_CLOSED.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "Renewal result: invoiceId, paid, expiresAt",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "description": "REACTIVATION_WINDOW_CLOSED — the service lapsed more than 14 days ago; renewal is no longer possible, order a new service. Also IDEMPOTENCY_IN_PROGRESS: a request with the same Idempotency-Key is still running; retry after Retry-After seconds with the same key."
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "periods": {
                    "type": "integer",
                    "enum": [
                      1,
                      3,
                      6,
                      12
                    ],
                    "default": 1,
                    "description": "ISP/GB proxies: how many billing periods to extend (bulk discounts: 3 → 5%, 6 → 10%, 12 → 20%)."
                  },
                  "months": {
                    "type": "integer",
                    "enum": [
                      1,
                      2,
                      3,
                      6,
                      12
                    ],
                    "default": 1,
                    "description": "Mobile proxies only: renewal term in months (1, 2, 3, 6 or 12; default 1). The console offers 1/2/3 as a top-up (renew again to stack past 3); each renewal extends the current expiry. 3/6/12 get Trump's prepaid-term discount (5/10/15%)."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/proxies/{id}/auth": {
      "get": {
        "tags": [
          "Proxies"
        ],
        "summary": "Proxy authentication settings",
        "description": "Auth method, proxy credentials (null when the service is IP-only) and the IP whitelist, plus the caller's detected IP. Applies to ISP and GB residential plans.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "Auth method, credentials, whitelisted IPs, yourIp"
          }
        }
      },
      "patch": {
        "tags": [
          "Proxies"
        ],
        "summary": "Switch the proxy authentication method",
        "description": "Sets the auth method: ip (whitelist only), password (credentials only) or combined. ISP plans only — GB residential plans are always combined.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "method"
                ],
                "properties": {
                  "method": {
                    "type": "string",
                    "enum": [
                      "ip",
                      "password",
                      "combined"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "The new method"
          }
        }
      }
    },
    "/proxies/{id}/auth/credentials": {
      "put": {
        "tags": [
          "Proxies"
        ],
        "summary": "Change the proxy credentials",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "username",
                  "password"
                ],
                "properties": {
                  "username": {
                    "type": "string",
                    "maxLength": 64
                  },
                  "password": {
                    "type": "string",
                    "maxLength": 64
                  }
                }
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "Credentials updated"
          }
        }
      }
    },
    "/proxies/{id}/auth/whitelisted-ips": {
      "post": {
        "tags": [
          "Proxies"
        ],
        "summary": "Whitelist a device IP",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ip"
                ],
                "properties": {
                  "ip": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "The whitelisted IP",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        }
      }
    },
    "/proxies/{id}/auth/whitelisted-ips/{ip}": {
      "delete": {
        "tags": [
          "Proxies"
        ],
        "summary": "Remove a whitelisted device IP",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "ip",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "Removed"
          }
        }
      }
    },
    "/proxies/{id}/auth/credentials/regenerate": {
      "post": {
        "tags": [
          "Proxies"
        ],
        "summary": "Regenerate proxy credentials (mobile non-VPN only)",
        "description": "Generate new random credentials for a mobile proxy line (4G/multisim only; VPN lines authenticate via their rotation link).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "New credentials"
          }
        }
      }
    },
    "/proxies/{id}/auto-rotation": {
      "get": {
        "tags": [
          "Proxies"
        ],
        "summary": "Fetch auto-rotation settings (mobile only)",
        "description": "Get the current auto-rotation interval configuration for a mobile proxy line.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Auto-rotation config"
          }
        }
      },
      "put": {
        "tags": [
          "Proxies"
        ],
        "summary": "Update auto-rotation settings (mobile only)",
        "description": "Set the auto-rotation interval (5-1440 minutes) for a mobile proxy line.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "enabled",
                  "intervalMinutes"
                ],
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  },
                  "intervalMinutes": {
                    "type": "integer",
                    "minimum": 5,
                    "maximum": 1440
                  }
                }
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "Updated auto-rotation config"
          }
        }
      }
    },
    "/proxies/{id}/vpn-config": {
      "get": {
        "tags": [
          "Proxies"
        ],
        "summary": "Download VPN config file (mobile-vpn only)",
        "description": "Download the OpenVPN (.ovpn) or WireGuard (.conf) connect file for a mobile-vpn line. The file is fetched on demand from the upstream provider and served as plain text under a RareCloud filename (Content-Disposition). A 404 means the config is not ready yet — retry shortly.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "VPN config file",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Proxy not found or no config available yet"
          }
        }
      }
    },
    "/proxies/residential/countries": {
      "get": {
        "tags": [
          "Proxies"
        ],
        "summary": "GB residential countries",
        "description": "The list of countries (id + name) selectable when creating a GB proxy-request.",
        "responses": {
          "200": {
            "description": "{ countries: [{ id, name }] }"
          }
        }
      }
    },
    "/proxies/residential/rotation-intervals": {
      "get": {
        "tags": [
          "Proxies"
        ],
        "summary": "GB residential rotation intervals",
        "description": "The available rotation intervals (id + label) for a GB proxy-request: all, high, 1min, 10min, 30min.",
        "responses": {
          "200": {
            "description": "{ rotationIntervals: [{ id, label }] }"
          }
        }
      }
    },
    "/proxies/{id}/proxy-requests": {
      "get": {
        "tags": [
          "Proxies"
        ],
        "summary": "List proxy-requests on a GB bucket",
        "description": "The proxy-requests (country + rotation + count groups) created on a GB Residential bucket you own.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "{ proxyRequests: [...] }"
          },
          "404": {
            "description": "Not found or not owned"
          }
        }
      },
      "post": {
        "tags": [
          "Proxies"
        ],
        "summary": "Create a proxy-request on a GB bucket",
        "description": "Allocates proxies from the bucket in a chosen country with a chosen rotation interval. Poll the request until active, then read its proxy-list.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "countryId",
                  "proxyCount",
                  "rotationInterval"
                ],
                "properties": {
                  "countryId": {
                    "type": "integer",
                    "description": "A country id from /proxies/residential/countries"
                  },
                  "proxyCount": {
                    "type": "integer",
                    "description": "How many proxies to allocate"
                  },
                  "rotationInterval": {
                    "type": "string",
                    "enum": [
                      "all",
                      "high",
                      "1min",
                      "10min",
                      "30min"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "{ proxyRequest: { id, status, ... } }",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "404": {
            "description": "Not found or not owned"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        }
      }
    },
    "/proxies/{id}/proxy-requests/{reqId}/proxy-list": {
      "get": {
        "tags": [
          "Proxies"
        ],
        "summary": "Proxy endpoints for a GB proxy-request",
        "description": "The live proxy list (ip, port, username, password) for an active proxy-request on a GB bucket you own.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "reqId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "{ proxies: [{ ip, port, username, password }] }"
          },
          "404": {
            "description": "Not found or not owned"
          }
        }
      }
    },
    "/proxies/{id}/proxy-requests/{reqId}/check": {
      "post": {
        "tags": [
          "Proxies"
        ],
        "summary": "Live-check a single GB proxy-request's proxies",
        "description": "Route a small request through each proxy in this request only (scoped per group) to report exit IP, ISP, city, country, up/down and gateway latency.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" }
          },
          {
            "name": "reqId",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "{ results: [{ ip, port, up, exitIp, isp, city, country, countryCode, latencyMs, routed }] }"
          }
        }
      }
    },
    "/proxies/{id}/proxy-requests/{reqId}": {
      "delete": {
        "tags": [
          "Proxies"
        ],
        "summary": "Delete a GB proxy-request",
        "description": "Removes a proxy-request from a GB bucket you own.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "reqId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "{ deleted: true }"
          },
          "404": {
            "description": "Not found or not owned"
          }
        }
      }
    },
    "/tokens": {
      "get": {
        "tags": [
          "Tokens"
        ],
        "summary": "List API tokens",
        "description": "Cookie auth only. Tokens cannot list other tokens.",
        "responses": {
          "200": {
            "description": "Tokens",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ApiToken"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Tokens"
        ],
        "summary": "Create a new API token",
        "description": "Cookie auth only. The secret is returned **once** in the response and never again.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "rateLimitRpm": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 600
                  },
                  "expiresAt": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true
                  }
                },
                "required": [
                  "name",
                  "scopes"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created with one-time secret reveal"
          }
        }
      }
    },
    "/tokens/{id}": {
      "delete": {
        "tags": [
          "Tokens"
        ],
        "summary": "Revoke an API token",
        "description": "Cookie auth only. Immediate effect.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked"
          }
        }
      }
    },
    "/catalog/products": {
      "get": {
        "tags": [
          "Catalog"
        ],
        "summary": "List orderable products",
        "description": "Public endpoint (un-authed). Filters narrow results.",
        "security": [],
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "backend",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "category",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CatalogProduct"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/catalog/products/{sku}": {
      "get": {
        "tags": [
          "Catalog"
        ],
        "summary": "Get product detail + all plans",
        "security": [],
        "parameters": [
          {
            "name": "sku",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "product": {
                          "$ref": "#/components/schemas/CatalogProduct"
                        },
                        "plans": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/CatalogPlan"
                          }
                        }
                      },
                      "required": [
                        "product",
                        "plans"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/catalog/products/{sku}/details": {
      "get": {
        "tags": [
          "Catalog"
        ],
        "summary": "Get live product detail (cycles + config options)",
        "description": "Public endpoint (un-authed). Live, per-product detail beyond the synced snapshot: the billing cycles that are actually available and the product's own configurable options (Region, Additional IPv4, CPU/RAM, OS template). For WHMCS-backed legacy products `cycles`/`configOptions` are populated from the live backend; for cloud (openstack/gardener) SKUs they are empty and the flavor specs live in `plans`.",
        "security": [],
        "parameters": [
          {
            "name": "sku",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/ProductDetails"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/catalog/regions": {
      "get": {
        "tags": [
          "Catalog"
        ],
        "summary": "List available regions",
        "security": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CatalogRegion"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/catalog/images": {
      "get": {
        "tags": [
          "Catalog"
        ],
        "summary": "List available OS images and marketplace apps",
        "description": "Public (no auth). Returns only images that can be deployed right now: every entry resolves to a bootable image on the platform, so any `slug` listed here is accepted as `imageId` by `POST /services` (deploy) and by reinstall. OS distros and marketplace apps share the list; marketplace apps have `os_family: \"app\"` and carry display copy (`summary`, `logo_slug`), and are listed only once their image exists. The console partitions the list by `os_family`.",
        "security": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CatalogImage"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/catalog/products/{sku}/os-templates": {
      "get": {
        "tags": [
          "Catalog"
        ],
        "summary": "List pre-purchase OS templates for a legacy/dedicated SKU",
        "description": "Public (no auth). The legacy Virtualizor OS templates a server/dedicated product can be deployed with, BEFORE purchase (the deploy wizard). Sourced from the catalog (single source of truth), resolved per product sub-kind (Linux / Windows / OpenClaw / n8n / dedicated). A-1: the template `slug` is the public os_name — never the Virtualizor internal osid. Returns an empty array when the SKU has no configured templates.",
        "security": [],
        "parameters": [
          {
            "name": "sku",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "templates": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "slug": {
                                "type": "string",
                                "example": "ubuntu-24.04-x86_64"
                              },
                              "display_name": {
                                "type": "string",
                                "example": "Ubuntu"
                              },
                              "version": {
                                "type": "string",
                                "example": "24.04 LTS"
                              }
                            },
                            "required": [
                              "slug",
                              "display_name"
                            ]
                          }
                        }
                      },
                      "required": [
                        "templates"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Product not found"
          }
        }
      }
    },
    "/catalog/listings/{category}": {
      "get": {
        "tags": [
          "Catalog"
        ],
        "summary": "List deploy-wizard product cards for a category",
        "description": "Public (no auth). Returns the structured product \"cards\" (sku, pricing[], specs, popular, tier, available_regions) that the deploy wizard renders, for a dashboard category (server | hosting | proxy | cloud-vm | cloud-k8s | …). A-1: the card `sku` is the public, prefix-free SKU — never the WHMCS pid / Nova flavor UUID. Hidden products are excluded.",
        "security": [],
        "parameters": [
          {
            "name": "category",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "cloud-vm",
                "cloud-k8s",
                "cloud-volume",
                "cloud-network",
                "cloud-loadbalancer",
                "cloud-reserved-ip",
                "server",
                "hosting",
                "proxy",
                "domain"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CatalogProductCard"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Unknown category"
          }
        }
      }
    },
    "/catalog/kubernetes-versions": {
      "get": {
        "tags": [
          "Catalog"
        ],
        "summary": "List offered managed-Kubernetes versions",
        "description": "Public (no auth). The managed-Kubernetes versions on offer, sourced from the catalog (single source of truth). Newest/default first. Returns an empty array when none are configured.",
        "security": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "versions": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "example": "1.32.0"
                          }
                        }
                      },
                      "required": [
                        "versions"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/orders": {
      "get": {
        "tags": [
          "Orders"
        ],
        "summary": "List your orders",
        "description": "Requires scope `billing:read`. The authenticated customer's orders (purchase records), newest first, each scoped to the caller. The order-create path is `POST /services`.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Order"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/orders/{id}": {
      "get": {
        "tags": [
          "Orders"
        ],
        "summary": "Get one of your orders",
        "description": "Requires scope `billing:read`. Returns the order only if it belongs to the authenticated caller; another user's order (or a non-numeric id) returns 404.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Order",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Order"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/services": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "List services",
        "description": "Lists every service the authenticated user owns across all backends (Promise.allSettled: one backend outage does not blank the list). When a backend could not be read, its resources are missing from this answer and the response carries `X-Partial-Results` naming the missing categories (for example `cloud-vm`); retry in a minute for the full list. No header means the list is complete.",
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "description": "Only list services of this category: cloud-vm, cloud-k8s, cloud-volume, cloud-network, cloud-loadbalancer, cloud-object-storage, server, hosting, proxy or domain. Omit to list everything.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "deprecated": true,
            "description": "Deprecated alias of `category`, same values. Send `category` instead. When both are given they must be equal, otherwise the request answers 400 INVALID_PARAM.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "X-Partial-Results": {
                "$ref": "#/components/headers/PartialResults"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Service"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "Deploy a service",
        "description": "Deploy a resource by API key (VPS / hosting / proxy). Places a real order and provisions it (e.g. Virtualizor). cloud-loadbalancer is asynchronous: it answers `status: \"provisioning\"` as soon as the load balancer exists, and `GET /services/{id}` reads `pending` until the build is done (`active`) or gave up (`error`). Accepts either {category, productId} or the CLI/Terraform shape {plan, name, image, sshKey}; a catalog SKU is resolved to the backend product id and the category inferred. Cloud (cloud-vm/cloud-k8s) is gated until the region is generally available. Requires scope services:write.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "category": {
                    "type": "string",
                    "enum": [
                      "server",
                      "hosting",
                      "proxy",
                      "domain",
                      "cloud-vm",
                      "cloud-k8s",
                      "cloud-volume",
                      "cloud-loadbalancer",
                      "cloud-network"
                    ],
                    "description": "Optional — inferred from the catalog product when omitted."
                  },
                  "productId": {
                    "type": "string",
                    "description": "Catalog SKU or backend product id."
                  },
                  "plan": {
                    "type": "string",
                    "description": "Alias for productId (CLI/Terraform)."
                  },
                  "region": {
                    "type": "string"
                  },
                  "billingCycle": {
                    "type": "string",
                    "enum": [
                      "monthly",
                      "quarterly",
                      "semiannually",
                      "annually",
                      "biennially",
                      "triennially",
                      "hourly"
                    ]
                  },
                  "hostname": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string",
                    "description": "Alias for hostname (CLI)."
                  },
                  "imageId": {
                    "type": "string"
                  },
                  "image": {
                    "type": "string",
                    "description": "Alias for imageId (CLI)."
                  },
                  "sshKeyId": {
                    "type": "string"
                  },
                  "sshKey": {
                    "type": "string",
                    "description": "Alias for sshKeyId (CLI)."
                  },
                  "sshPublicKey": {
                    "type": "string",
                    "description": "cloud-vm: raw SSH public-key material injected into the VM via cloud-init user_data (not a Nova key_name)."
                  },
                  "rootPassword": {
                    "type": "string",
                    "description": "cloud-vm: initial root password injected via cloud-init user_data."
                  },
                  "k8sVersion": {
                    "type": "string",
                    "description": "cloud-k8s: Kubernetes version for the managed cluster. Must be one of GET /catalog/kubernetes-versions, otherwise the request is refused with INVALID_PARAM listing the offered versions. When omitted, the catalog default (the first, newest supported version in that list) is used."
                  },
                  "machineType": {
                    "type": "string",
                    "description": "cloud-k8s: worker machine type / flavor. Falls back to the default when omitted."
                  },
                  "workerMin": {
                    "type": "integer",
                    "description": "cloud-k8s: minimum worker node count of the legacy single pool (node-pool autoscaling lower bound). Ignored when pools[] is supplied."
                  },
                  "workerMax": {
                    "type": "integer",
                    "description": "cloud-k8s: maximum worker node count of the legacy single pool (node-pool autoscaling upper bound). Ignored when pools[] is supplied."
                  },
                  "pools": {
                    "type": "array",
                    "maxItems": 5,
                    "description": "cloud-k8s: up to 5 user-named node pools to create the cluster with. When present, supersedes the legacy single-pool workerMin/workerMax. Each pool's machineType is resolved server-side (catalog SKU or CloudProfile flavor name); a bogus value is rejected.",
                    "items": {
                      "type": "object",
                      "required": [
                        "name",
                        "minimum",
                        "maximum"
                      ],
                      "properties": {
                        "name": {
                          "type": "string",
                          "description": "Node-pool name (lowercase letters/digits/hyphens, start with a letter, max 15; unique within the cluster)."
                        },
                        "machineType": {
                          "type": "string",
                          "description": "Worker machine type / flavor for this pool. Defaults to the cluster/SKU default when omitted."
                        },
                        "minimum": {
                          "type": "integer",
                          "description": "Minimum worker count for this pool (autoscaling lower bound)."
                        },
                        "maximum": {
                          "type": "integer",
                          "description": "Maximum worker count for this pool (>= minimum, <= 16)."
                        },
                        "volumeSizeGb": {
                          "type": "integer",
                          "minimum": 10,
                          "maximum": 1000,
                          "description": "Per-node root-volume size in GiB for this pool. Defaults to 30 when omitted."
                        }
                      }
                    }
                  },
                  "registry": {
                    "type": "object",
                    "description": "cloud-k8s: link this cluster to your container registry as soon as it becomes reachable (Container Registry, Plan 3, spec §6B). Ignored (not an error) when the registry is not enabled for the account, the response's `registry.reason` says why. Additive; omitted by every existing caller.",
                    "properties": {
                      "link": {
                        "type": "boolean"
                      }
                    },
                    "required": [
                      "link"
                    ]
                  },
                  "port": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 65535,
                    "description": "cloud-loadbalancer: the L4 TCP listener/member port. Defaults to 80."
                  },
                  "memberServerIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "cloud-loadbalancer: Nova server ids to balance traffic across."
                  },
                  "healthCheck": {
                    "type": "boolean",
                    "description": "cloud-loadbalancer: enable member health checks. Defaults to on."
                  },
                  "sizeGb": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 2048,
                    "description": "cloud-volume: block-volume size in GB (per-GB priced, not a catalog SKU). Required for category cloud-volume."
                  },
                  "addons": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Optional add-on slugs to include with the order."
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Optional tags. cloud-vm: stored on the VM (returned as the Service's `tags`, changed later with PUT /services/{id}/tags); at most 50 tags, each 1 to 60 characters without `,` `/` or control characters, duplicates dropped; tags starting with `managed:`, `k8s:` or `rarecloud` (any case) are reserved and rejected (INVALID_PARAM). Ignored for other categories."
                  },
                  "vpcId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "cloud-vm: the VPC (cloud-network) to launch the VM into."
                  },
                  "configOptions": {
                    "type": "object",
                    "additionalProperties": {
                      "oneOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "number"
                        }
                      ]
                    },
                    "description": "Legacy WHMCS config-option selections ({ configOptionId: optionId|qty }) for server/hosting/proxy products. Forwarded verbatim to placeOrder."
                  },
                  "customFields": {
                    "type": "object",
                    "additionalProperties": {
                      "oneOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "number"
                        }
                      ]
                    },
                    "description": "Legacy WHMCS custom fields ({ customFieldId: value }), e.g. the Operating System field the Virtualizor module provisions from. Forwarded verbatim to placeOrder."
                  },
                  "payWith": {
                    "type": "string",
                    "description": "'credit' (default), a gateway slug, or 'invoice'."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "serviceId": {
                          "type": "string"
                        },
                        "invoiceId": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "provisioning",
                            "pending-payment",
                            "failed"
                          ]
                        },
                        "publicIp": {
                          "type": "string",
                          "description": "Public IPv4 of a newly provisioned cloud VM, when one attached at create time."
                        },
                        "consolePassword": {
                          "type": "string",
                          "description": "One-time, auto-generated CONSOLE-ONLY (VNC) root password for a cloud VM created with SSH-key or no auth (when the caller did NOT supply a rootPassword). Returned ONCE on create and never again; it works only on the web/VNC console and NOT over SSH. Absent when the caller supplied their own rootPassword (which then works over SSH too when no SSH key was provided; with a key, SSH stays key-only and the password is console-only)."
                        },
                        "registry": {
                          "type": "object",
                          "description": "cloud-k8s only, present only when the request's `registry.link` was true. Whether the create-time container-registry link was recorded. A failure to record it (e.g. a transient database error) is reported here rather than failing the whole create, the cluster itself was already provisioned by this point.",
                          "properties": {
                            "linked": {
                              "type": "boolean"
                            },
                            "status": {
                              "type": "string",
                              "enum": [
                                "linking",
                                "active",
                                "rotating",
                                "unlinking",
                                "error"
                              ],
                              "description": "Present when linked is true, the freshly created link row's status (normally \"linking\"; the hourly reconciler installs the syncer once the cluster is reachable)."
                            },
                            "reason": {
                              "type": "string",
                              "enum": [
                                "not_enabled",
                                "link_failed",
                                "not_available"
                              ],
                              "description": "Present when linked is false. not_enabled: this account has no container registry. link_failed: the link could not be recorded. not_available: the container registry is not launched yet, so no link was recorded. The cluster itself is created in every case."
                            }
                          },
                          "required": [
                            "linked"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/api-access": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "List resources that are read-only for API tokens",
        "description": "Every resource whose API access is off (read-only for API tokens and agents), so an agent can check in one call what it may not change. Any authenticated caller, no scope required. `kind` is `service` (anything listed by `GET /services` or `GET /proxies`) or `domain`.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "kind": {
                            "type": "string",
                            "enum": [
                              "service",
                              "domain"
                            ]
                          },
                          "id": {
                            "type": "string"
                          },
                          "changedAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        },
                        "required": [
                          "kind",
                          "id",
                          "changedAt"
                        ]
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/services/preflight": {
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "Check whether an order would be accepted",
        "description": "Answers \"would POST /services accept this order for me right now?\" without placing it. Takes the same body as POST /services and runs the same order-balance check the real deploy runs, as a dry run: nothing is created, held, reserved or charged, and no audit entry is written. It always evaluates the caller's own account.\n\nThe rule it applies to hourly cloud resources (cloud-vm, cloud-k8s, cloud-volume, cloud-loadbalancer): an account with an overdue cloud usage invoice is refused; an established account (registered for a while, with payments received) and internal accounts may order without a balance; every other account needs a spendable balance that covers the first month of every cloud resource it already runs plus the new one.\n\ncloud-network is free and always allowed (reason `free`). Legacy WHMCS orders (server, hosting, proxy, domain) are priced and checked by WHMCS when the order is placed, so they answer allowed with reason `checked_at_order`; POST /services can still refuse them.\n\nThe answer is advice for the moment it is asked: POST /services decides again when the order is placed (another order may have used the balance in between). Requires scope services:write, the same as the deploy it previews. Not idempotency-keyed: it creates nothing.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "category": {
                    "type": "string",
                    "enum": [
                      "server",
                      "hosting",
                      "proxy",
                      "domain",
                      "cloud-vm",
                      "cloud-k8s",
                      "cloud-volume",
                      "cloud-loadbalancer",
                      "cloud-network"
                    ],
                    "description": "Optional — inferred from the catalog product when omitted."
                  },
                  "productId": {
                    "type": "string",
                    "description": "Catalog SKU or backend product id."
                  },
                  "plan": {
                    "type": "string",
                    "description": "Alias for productId (CLI/Terraform)."
                  },
                  "region": {
                    "type": "string"
                  },
                  "billingCycle": {
                    "type": "string",
                    "enum": [
                      "monthly",
                      "quarterly",
                      "semiannually",
                      "annually",
                      "biennially",
                      "triennially",
                      "hourly"
                    ]
                  },
                  "hostname": {
                    "type": "string"
                  },
                  "name": {
                    "type": "string",
                    "description": "Alias for hostname (CLI)."
                  },
                  "imageId": {
                    "type": "string"
                  },
                  "image": {
                    "type": "string",
                    "description": "Alias for imageId (CLI)."
                  },
                  "sshKeyId": {
                    "type": "string"
                  },
                  "sshKey": {
                    "type": "string",
                    "description": "Alias for sshKeyId (CLI)."
                  },
                  "sshPublicKey": {
                    "type": "string",
                    "description": "cloud-vm: raw SSH public-key material injected into the VM via cloud-init user_data (not a Nova key_name)."
                  },
                  "rootPassword": {
                    "type": "string",
                    "description": "cloud-vm: initial root password injected via cloud-init user_data."
                  },
                  "k8sVersion": {
                    "type": "string",
                    "description": "cloud-k8s: Kubernetes version for the managed cluster. Must be one of GET /catalog/kubernetes-versions, otherwise the request is refused with INVALID_PARAM listing the offered versions. When omitted, the catalog default (the first, newest supported version in that list) is used."
                  },
                  "machineType": {
                    "type": "string",
                    "description": "cloud-k8s: worker machine type / flavor. Falls back to the default when omitted."
                  },
                  "workerMin": {
                    "type": "integer",
                    "description": "cloud-k8s: minimum worker node count of the legacy single pool (node-pool autoscaling lower bound). Ignored when pools[] is supplied."
                  },
                  "workerMax": {
                    "type": "integer",
                    "description": "cloud-k8s: maximum worker node count of the legacy single pool (node-pool autoscaling upper bound). Ignored when pools[] is supplied."
                  },
                  "pools": {
                    "type": "array",
                    "maxItems": 5,
                    "description": "cloud-k8s: up to 5 user-named node pools to create the cluster with. When present, supersedes the legacy single-pool workerMin/workerMax. Each pool's machineType is resolved server-side (catalog SKU or CloudProfile flavor name); a bogus value is rejected.",
                    "items": {
                      "type": "object",
                      "required": [
                        "name",
                        "minimum",
                        "maximum"
                      ],
                      "properties": {
                        "name": {
                          "type": "string",
                          "description": "Node-pool name (lowercase letters/digits/hyphens, start with a letter, max 15; unique within the cluster)."
                        },
                        "machineType": {
                          "type": "string",
                          "description": "Worker machine type / flavor for this pool. Defaults to the cluster/SKU default when omitted."
                        },
                        "minimum": {
                          "type": "integer",
                          "description": "Minimum worker count for this pool (autoscaling lower bound)."
                        },
                        "maximum": {
                          "type": "integer",
                          "description": "Maximum worker count for this pool (>= minimum, <= 16)."
                        },
                        "volumeSizeGb": {
                          "type": "integer",
                          "minimum": 10,
                          "maximum": 1000,
                          "description": "Per-node root-volume size in GiB for this pool. Defaults to 30 when omitted."
                        }
                      }
                    }
                  },
                  "registry": {
                    "type": "object",
                    "description": "cloud-k8s: link this cluster to your container registry as soon as it becomes reachable (Container Registry, Plan 3, spec §6B). Ignored (not an error) when the registry is not enabled for the account, the response's `registry.reason` says why. Additive; omitted by every existing caller.",
                    "properties": {
                      "link": {
                        "type": "boolean"
                      }
                    },
                    "required": [
                      "link"
                    ]
                  },
                  "port": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 65535,
                    "description": "cloud-loadbalancer: the L4 TCP listener/member port. Defaults to 80."
                  },
                  "memberServerIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "cloud-loadbalancer: Nova server ids to balance traffic across."
                  },
                  "healthCheck": {
                    "type": "boolean",
                    "description": "cloud-loadbalancer: enable member health checks. Defaults to on."
                  },
                  "sizeGb": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 2048,
                    "description": "cloud-volume: block-volume size in GB (per-GB priced, not a catalog SKU). Required for category cloud-volume."
                  },
                  "addons": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Optional add-on slugs to include with the order."
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Optional tags. cloud-vm: stored on the VM (returned as the Service's `tags`, changed later with PUT /services/{id}/tags); at most 50 tags, each 1 to 60 characters without `,` `/` or control characters, duplicates dropped; tags starting with `managed:`, `k8s:` or `rarecloud` (any case) are reserved and rejected (INVALID_PARAM). Ignored for other categories."
                  },
                  "vpcId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "cloud-vm: the VPC (cloud-network) to launch the VM into."
                  },
                  "configOptions": {
                    "type": "object",
                    "additionalProperties": {
                      "oneOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "number"
                        }
                      ]
                    },
                    "description": "Legacy WHMCS config-option selections ({ configOptionId: optionId|qty }) for server/hosting/proxy products. Forwarded verbatim to placeOrder."
                  },
                  "customFields": {
                    "type": "object",
                    "additionalProperties": {
                      "oneOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "number"
                        }
                      ]
                    },
                    "description": "Legacy WHMCS custom fields ({ customFieldId: value }), e.g. the Operating System field the Virtualizor module provisions from. Forwarded verbatim to placeOrder."
                  },
                  "payWith": {
                    "type": "string",
                    "description": "'credit' (default), a gateway slug, or 'invoice'."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "required": [
                        "allowed",
                        "reason"
                      ],
                      "properties": {
                        "allowed": {
                          "type": "boolean",
                          "description": "true when POST /services would accept this order now."
                        },
                        "reason": {
                          "type": "string",
                          "enum": [
                            "ok",
                            "guard_off",
                            "admin",
                            "established",
                            "free",
                            "checked_at_order",
                            "no_billing_account",
                            "overdue_invoice",
                            "insufficient_balance"
                          ],
                          "description": "ok: the balance covers what the account runs plus this order. guard_off: the balance check is switched off platform-wide. admin: an internal account. established: an established account orders without a balance. free: this resource costs nothing. checked_at_order: a legacy WHMCS order, checked when it is placed. no_billing_account: the account has no billing account. overdue_invoice: a cloud usage invoice is overdue; pay it first. insufficient_balance: add funds first. New values may be added; treat an unknown value with allowed=false as a refusal and show `message`."
                        },
                        "neededCents": {
                          "type": "integer",
                          "description": "ok / insufficient_balance: what the balance has to cover (first month of what already runs, less this month's usage already taken, plus this order), in minor units of `currency`."
                        },
                        "availableCents": {
                          "type": "integer",
                          "description": "ok / insufficient_balance: the spendable balance (credit + promo bonus - this month's uninvoiced usage), in minor units of `currency`."
                        },
                        "missingCents": {
                          "type": "integer",
                          "description": "insufficient_balance: how much is missing, in minor units of `currency`."
                        },
                        "currency": {
                          "type": "string",
                          "description": "The account currency (ISO code). Present when the balance was read."
                        },
                        "addFundsUrl": {
                          "type": "string",
                          "format": "uri",
                          "description": "insufficient_balance: the console Add funds page with the missing amount prefilled (whole units, at least the minimum top-up)."
                        },
                        "invoiceId": {
                          "type": "integer",
                          "description": "overdue_invoice, callers with billing:read only: the overdue invoice."
                        },
                        "payInvoiceUrl": {
                          "type": "string",
                          "format": "uri",
                          "description": "overdue_invoice, callers with billing:read only: the console page where the invoice is paid."
                        },
                        "message": {
                          "type": "string",
                          "description": "When allowed is false: the exact message POST /services would fail with."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body is invalid (same validation as POST /services), e.g. no productId/plan, or a cloud-volume without sizeGb."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "description": "What the account already runs could not be read, so no answer can be given. Retry; do not treat this as allowed."
          }
        }
      }
    },
    "/services/{id}": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "Get service detail",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "503": {
            "$ref": "#/components/responses/BackendUnavailable"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Service"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Services"
        ],
        "summary": "Destroy a service",
        "description": "Destroy / deprovision a resource. The deploy -> destroy -> redeploy lifecycle. A numeric id is a legacy VPS (deletes the Virtualizor VM + disk). A DNS-style id is a managed Kubernetes cluster. A UUID is whichever of these the caller owns under it, tried in this order: cloud VM, load balancer, volume, private network. Each kind is deleted with the same ownership checks, refusals and behaviour as its own route (DELETE /load-balancers/{id}, /volumes/{id}, /networks/{id}; a load balancer that is still being built is accepted with `status: \"deleting\"` and deleted in the background): a Kubernetes-managed load balancer, a private network with VMs on it, the default network and a cluster-owned volume are refused there and here alike. An object storage bucket UUID the caller owns is not deleted here: it is refused with INVALID_PARAM pointing at DELETE /object-storage/buckets/{id}?confirm=<full bucket name>. A UUID the caller owns as none of these is NOT_FOUND. Requires scope services:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "confirm",
            "in": "query",
            "required": false,
            "description": "Volumes only: the volume name, typed to confirm deleting a volume whose Kubernetes cluster no longer exists (the same as DELETE /volumes/{id}?confirm=). Ignored for every other kind.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "deleted",
                            "deleting"
                          ],
                          "description": "Load balancers only (absent for every other kind): `deleted`, or `deleting` when the load balancer was still being built and is deleted in the background, exactly as DELETE /load-balancers/{id} answers."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/services/{id}/api-access": {
      "put": {
        "tags": [
          "Services"
        ],
        "summary": "Set API access for a resource (console only)",
        "description": "Turn API access for one resource on (`full`) or off (`read_only`). When off, API tokens and agents (MCP, CLI, Terraform, scripts) can still list and read the resource, but every change to it (stop, reboot, resize, reinstall, delete, attach/detach of volumes, firewalls, reserved IPs and networks, load balancer members, ...) and every read that returns a credential (kubeconfigs, VNC console URL, panel SSO links, proxy endpoints/credentials/auth settings, VPN config) is refused with 403 `RESOURCE_PROTECTED`. Console sessions keep full control. **Console session only**: an API token gets 403 `FORBIDDEN`, so an agent can never switch protection off. `id` is the id the public API uses for the resource: a numeric legacy service id, a cloud VM/volume/network/load balancer uuid, a Kubernetes cluster name, or a `/proxies` id. Another account's id is 404, exactly like the other routes. Every change is written to the account audit log (`resource.api_access`, old and new value). Returns the updated object.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "apiAccess": {
                    "type": "string",
                    "enum": [
                      "full",
                      "read_only"
                    ],
                    "description": "`read_only` makes the resource read-only for API tokens and agents; `full` turns API access back on."
                  }
                },
                "required": [
                  "apiAccess"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Service"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid body, or a resource kind without the switch yet (object storage).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Called with an API token. Error code `FORBIDDEN`, message \"API access can only be changed in the console.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/services/{id}/actions/{action}": {
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "Trigger service action",
        "description": "Allowed actions: start, stop, reboot, reinstall, reset-password. Power actions (start/stop/reboot) are backend-aware: a cloud VM (Nova UUID id) is powered via OpenStack, a legacy VPS (numeric id) via WHMCS/Virtualizor. reinstall rebuilds the VM from scratch (destructive); for a cloud VM (Nova UUID id), supply { imageId, password?, sshPublicKey? } — imageId is a curated slug from GET /services/{id}/os-templates; the VM keeps the same IP; if no password is supplied the response data includes a one-time consolePassword. For a legacy VPS (numeric id), supply { imageId, password }. reset-password (cloud VM only, non-destructive) changes the root password on a RUNNING VM live via Nova os-set-password + qemu-guest-agent — the new password always works on the VNC console; over SSH it works only on VMs deployed/reinstalled with password authentication (SSH-key VMs stay key-only); supply { password } (8–128 chars); the VM keeps running and its data. Only offered when the Service's capabilities.resetRootPassword is true.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "action",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "start",
                "stop",
                "reboot",
                "reinstall",
                "reset-password"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "object",
                    "description": "Cloud VM reinstall (Nova UUID id)",
                    "properties": {
                      "imageId": {
                        "type": "string",
                        "description": "Curated image slug (e.g. ubuntu-24.04) — see GET /services/{id}/os-templates"
                      },
                      "password": {
                        "type": "string",
                        "description": "Root password to set on the rebuilt VM"
                      },
                      "sshPublicKey": {
                        "type": "string",
                        "description": "Inline SSH public key to install for root"
                      }
                    },
                    "required": [
                      "imageId"
                    ],
                    "additionalProperties": false
                  },
                  {
                    "type": "object",
                    "description": "Legacy VPS reinstall (numeric id)",
                    "properties": {
                      "imageId": {
                        "type": "string",
                        "maxLength": 128,
                        "description": "OS template id (see GET /services/{id}/os-templates)"
                      },
                      "password": {
                        "type": "string",
                        "minLength": 8,
                        "maxLength": 128,
                        "description": "Root password (optional, 8-128 chars)"
                      }
                    },
                    "required": [
                      "imageId"
                    ],
                    "additionalProperties": false
                  },
                  {
                    "type": "object",
                    "description": "Cloud VM reset-password (Nova UUID id) — live root-password reset via qemu-guest-agent",
                    "properties": {
                      "password": {
                        "type": "string",
                        "minLength": 8,
                        "maxLength": 128,
                        "description": "New root password to apply to the running VM (8–128 chars)"
                      }
                    },
                    "required": [
                      "password"
                    ],
                    "additionalProperties": false
                  },
                  {
                    "type": "object",
                    "description": "Power action (start/stop/reboot) — no request body / no imageId",
                    "additionalProperties": false
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "Action queued or completed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "action": {
                      "type": "string",
                      "description": "The action that was triggered (e.g. reinstall)"
                    },
                    "imageName": {
                      "type": "string",
                      "description": "The new OS image name — returned ONLY for the reinstall action."
                    },
                    "consolePassword": {
                      "type": "string",
                      "description": "Auto-generated console (VNC) password — returned ONCE and ONLY for the reinstall action when no password was supplied in the request."
                    },
                    "status": {
                      "type": "string",
                      "description": "Action status — returned for the reset-password action as 'reset-requested' (Nova accepted the change; the guest applies it asynchronously)."
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        }
      }
    },
    "/account/limits": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Get resource limits + current usage",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LimitsRow"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/services/{id}/metrics": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/metrics",
        "description": "Resource-usage metrics for a service. Cloud VMs (UUID id) return CPU/memory/disk/network time-series over ?range (1h|24h|7d); legacy VPS (numeric id) return a point-in-time snapshot.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "oneOf": [
                        {
                          "type": "object",
                          "description": "Cloud VM: resource-usage time-series over the requested range.",
                          "properties": {
                            "range": {
                              "type": "string",
                              "enum": [
                                "1h",
                                "24h",
                                "7d"
                              ]
                            },
                            "step": {
                              "type": "integer",
                              "description": "Seconds between points."
                            },
                            "series": {
                              "type": "object",
                              "description": "Per-metric arrays of [unixSeconds, value|null] pairs.",
                              "properties": {
                                "cpu_pct": {
                                  "type": "array",
                                  "items": {
                                    "type": "array"
                                  }
                                },
                                "mem_used_pct": {
                                  "type": "array",
                                  "items": {
                                    "type": "array"
                                  }
                                },
                                "mem_total_bytes": {
                                  "type": "array",
                                  "items": {
                                    "type": "array"
                                  }
                                },
                                "disk_read_bps": {
                                  "type": "array",
                                  "items": {
                                    "type": "array"
                                  }
                                },
                                "disk_write_bps": {
                                  "type": "array",
                                  "items": {
                                    "type": "array"
                                  }
                                },
                                "net_rx_bps": {
                                  "type": "array",
                                  "items": {
                                    "type": "array"
                                  }
                                },
                                "net_tx_bps": {
                                  "type": "array",
                                  "items": {
                                    "type": "array"
                                  }
                                }
                              }
                            },
                            "latest": {
                              "type": "object",
                              "additionalProperties": {
                                "type": [
                                  "number",
                                  "null"
                                ]
                              }
                            },
                            "source": {
                              "type": "string"
                            },
                            "unavailable": {
                              "type": "boolean"
                            }
                          }
                        },
                        {
                          "type": "object",
                          "description": "Legacy VPS: point-in-time snapshot.",
                          "properties": {
                            "cpu_util_pct": {
                              "type": [
                                "number",
                                "null"
                              ]
                            },
                            "ram_used_mb": {
                              "type": [
                                "number",
                                "null"
                              ]
                            },
                            "disk_used_gb": {
                              "type": [
                                "number",
                                "null"
                              ]
                            },
                            "network_rx_bytes": {
                              "type": [
                                "number",
                                "null"
                              ]
                            },
                            "network_tx_bytes": {
                              "type": [
                                "number",
                                "null"
                              ]
                            }
                          }
                        }
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "range",
            "in": "query",
            "required": false,
            "description": "Cloud VMs only: time window for the graphs.",
            "schema": {
              "type": "string",
              "enum": [
                "1h",
                "24h",
                "7d"
              ],
              "default": "1h"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/iso": {
      "delete": {
        "tags": [
          "Services"
        ],
        "summary": "DELETE /services/{id}/iso",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/iso",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "put": {
        "tags": [
          "Services"
        ],
        "summary": "PUT /services/{id}/iso",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "iso_url": {
                    "type": "string",
                    "maxLength": 2048,
                    "description": "URL the backend mounts as a virtual CD-ROM. Fetched server-side (SSRF-guarded against private/metadata targets)."
                  }
                },
                "required": [
                  "iso_url"
                ]
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/ssh-keys": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/ssh-keys",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "POST /services/{id}/ssh-keys",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "public_key": {
                    "type": "string",
                    "maxLength": 4096
                  },
                  "id": {
                    "type": "string",
                    "maxLength": 64
                  }
                },
                "required": [
                  "public_key"
                ]
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/ssh-keys/library": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/ssh-keys/library",
        "description": "List the SSH keys registered in a legacy VPS's key library (Virtualizor). Each entry has id, name, publicKey, and a server-computed fingerprint. Distinct from GET /services/{id}/ssh-keys (the gateway-adapter contract). Owner-scoped — a service you don't own → NOT_FOUND. Requires scope services:read.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "POST /services/{id}/ssh-keys/library",
        "description": "Add a new SSH key to a legacy VPS's key library: {name, key}. Owner-scoped — a service you don't own → NOT_FOUND. Requires scope services:write.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "key": {
                    "type": "string",
                    "maxLength": 4096
                  }
                },
                "required": [
                  "name",
                  "key"
                ]
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/ssh-keys/library/apply": {
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "POST /services/{id}/ssh-keys/library/apply",
        "description": "Apply a selected SET of library SSH keys to a legacy VPS: {keyIds}. Replaces the keys installed on the VPS with the chosen set. Owner-scoped — a service you don't own → NOT_FOUND. Requires scope services:write.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "keyIds": {
                    "type": "array",
                    "maxItems": 50,
                    "default": [],
                    "items": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 64
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/backups": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/backups",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "POST /services/{id}/backups",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/vpanel": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/vpanel — native Virtualizor management panel shell (HTML) for an owned VPS, embedded in the console (replaces the WHMCS iframe). Ownership-scoped.",
        "responses": {
          "200": {
            "description": "Panel shell HTML.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/vpanel/novnc": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/vpanel/novnc — the HTML5 (noVNC) browser console viewer for an owned legacy VPS. Serves the Virtualizor noVNC viewer; the browser then connects directly to the node's websockify (wss://<node>:4083/novnc). Returns an honest notice when the server is offline / VNC isn't ready. Ownership-scoped; cookie-only (browser feature).",
        "responses": {
          "200": {
            "description": "noVNC console viewer HTML (or an honest 'server offline' notice).",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/vpanel/novnc-assets/{path}": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/vpanel/novnc-assets/{path} — same-origin proxy for a whitelisted noVNC static asset (app/core/vendor/po under the module's novnc/ dir) for an owned legacy VPS. Loaded automatically by the noVNC viewer's <script type=\"module\"> and its relative imports so they resolve same-origin (cross-origin ES modules require CORS, which the WHMCS asset host doesn't send). Ownership-scoped; cookie-only (browser feature); path is strictly whitelisted, no traversal.",
        "responses": {
          "200": {
            "description": "The requested static asset, streamed through with the matching Content-Type."
          },
          "404": {
            "description": "Unknown/whitelisted-out path, or the asset doesn't exist upstream."
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "path",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Asset sub-path under novnc/, e.g. app/ui.js or core/rfb.js."
          }
        ],
        "security": [
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/vpanel/ui-assets/{path}": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/vpanel/ui-assets/{path} — same-origin proxy for a whitelisted panel font asset (css2/ + fonts/ under the module's ui/ dir) for an owned service. The panel's style.css @font-face rules reference fonts cross-origin under the WHMCS host; @font-face sources are fetched in CORS mode and the host sends no Access-Control-Allow-Origin, so the fonts are blocked. The style.css bundle rewrites its font url()s to this proxy so they load same-origin. Ownership-scoped; cookie-only (browser feature); path is strictly whitelisted, no traversal.",
        "responses": {
          "200": {
            "description": "The requested font asset, streamed through with the matching Content-Type."
          },
          "404": {
            "description": "Unknown/whitelisted-out path, or the asset doesn't exist upstream."
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "path",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Font asset sub-path under css2/ or fonts/, e.g. css2/fonts/inter/Inter-Bold.woff2 or fonts/fa-solid-900.woff2."
          }
        ],
        "security": [
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/vpanel/sso": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/vpanel/sso — one-shot SSO auto-login URL into the customer's Virtualizor enduser panel for an owned VPS (the browser opens it in a new tab). Ownership-scoped; cookie-only.",
        "responses": {
          "200": {
            "description": "The auto-login URL.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "url": {
                          "type": "string",
                          "description": "One-shot Virtualizor enduser auto-login URL; open it in a new browser tab."
                        }
                      },
                      "required": [
                        "url"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/vpanel/act": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/vpanel/act — proxy a Virtualizor enduser API action (act=) for an owned VPS; the master key is added server-side. Passthrough JSON.",
        "responses": {
          "200": {
            "description": "Passthrough Virtualizor enduser API response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "act",
            "in": "query",
            "required": true,
            "description": "Virtualizor enduser API action name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "POST /services/{id}/vpanel/act — proxy a state-changing Virtualizor enduser API action (act=) for an owned VPS; the master key is added server-side. Passthrough JSON.",
        "responses": {
          "200": {
            "description": "Passthrough Virtualizor enduser API response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "act",
            "in": "query",
            "required": true,
            "description": "Virtualizor enduser API action name.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/console": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/console",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/console/ws": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/console/ws — noVNC WebSocket proxy (NOT YET AVAILABLE — returns 426; ships in 4.5.1). Use POST .../console for the SSO console URL meanwhile.",
        "responses": {
          "426": {
            "description": "Upgrade Required — the WebSocket console proxy is not implemented yet.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        false
                      ]
                    },
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/billing/credit/ledger": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "GET /billing/credit/ledger",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/billing/credit/top-up": {
      "post": {
        "tags": [
          "Billing"
        ],
        "summary": "POST /billing/credit/top-up",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/billing/invoices/{id}/pay": {
      "post": {
        "tags": [
          "Billing"
        ],
        "summary": "POST /billing/invoices/{id}/pay",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/billing/invoices/{id}/pay-preview": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "GET /billing/invoices/{id}/pay-preview",
        "description": "Read-only breakdown of what paying this invoice from the account balance would use: promo bonus first, then real credit, plus any shortfall. Consumes nothing.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "invoiceId": {
                          "type": "string"
                        },
                        "currency": {
                          "type": "string"
                        },
                        "grandTotalCents": {
                          "type": "integer",
                          "description": "What the invoice bills BEFORE any credit is applied (subtotal + tax + tax2). Renamed from `totalCents` (removed 2026-08): that field carried WHMCS `total`, which is the residual AFTER credit, so it is a different number on any credit-carrying invoice. For what is still owed, read `balanceCents`."
                        },
                        "balanceCents": {
                          "type": "integer",
                          "description": "Amount still owed on the invoice — the grand total less any credit already applied to it."
                        },
                        "bonusAvailableCents": {
                          "type": "integer"
                        },
                        "creditAvailableCents": {
                          "type": "integer"
                        },
                        "bonusWillApplyCents": {
                          "type": "integer"
                        },
                        "creditWillApplyCents": {
                          "type": "integer"
                        },
                        "shortfallCents": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/billing/vouchers/redeem": {
      "post": {
        "tags": [
          "Billing"
        ],
        "summary": "POST /billing/vouchers/redeem",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/services/{id}/provisioning": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/provisioning — Setup state of a pending service (paid vs unpaid vs stuck)",
        "description": "Works for legacy VPS ids (numeric) and cloud VM UUIDs. Poll it after a deploy until `provisioned` is true. For a cloud VM the state comes from OpenStack in the same shape: `provisioned` is false while the server builds and on a failed build, true once it has been built (running or stopped); `paymentStatus` is always `paid` and `orderStatus` `active` because a cloud VM is usage-billed with no order to wait on, and `orderId` is omitted; `stuck` is true for a failed build or a build still running after 15 minutes. A cloud VM UUID you do not own returns NOT_FOUND, the same as one that does not exist. Requires the `services:read` scope.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/os-templates": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/os-templates — Images this service can be reinstalled with. For a cloud VM (Nova UUID id): the curated stock catalog PLUS your own ready snapshots, each entry { id, name, kind } where kind is 'stock' or 'snapshot'. For a legacy VPS (numeric id): the Virtualizor OS templates, { id, name }.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/kubeconfig": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/kubeconfig — Short-lived admin kubeconfig for a managed Kubernetes cluster (your own only)",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "kubeconfig": {
                          "type": "string",
                          "description": "The admin kubeconfig as a YAML document."
                        },
                        "expiresAt": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When the embedded credential expires (ISO 8601)."
                        }
                      },
                      "required": [
                        "kubeconfig",
                        "expiresAt"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/kubeconfigs": {
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "POST /services/{id}/kubeconfigs — Create a long-lived kubeconfig credential (per-credential ServiceAccount in your cluster; revocable)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 64,
                    "description": "A label for the credential (shown in the list)."
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "admin",
                      "view"
                    ],
                    "description": "In-cluster access level: admin (cluster-admin) or view (read-only)."
                  },
                  "ttl": {
                    "type": "string",
                    "enum": [
                      "30d",
                      "90d",
                      "1y",
                      "never"
                    ],
                    "default": "90d",
                    "description": "Token lifetime. \"never\" mints a 10-year token (expiresAt is null); revoke to end it."
                  }
                },
                "required": [
                  "name",
                  "role"
                ]
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Credential id (use it to download or revoke)."
                        },
                        "name": {
                          "type": "string"
                        },
                        "role": {
                          "type": "string",
                          "enum": [
                            "admin",
                            "view"
                          ]
                        },
                        "createdAt": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "expiresAt": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "When the token expires (ISO 8601); null when ttl is \"never\"."
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "active",
                            "expired",
                            "revoking",
                            "revoked"
                          ]
                        },
                        "kubeconfig": {
                          "type": "string",
                          "description": "The ready-to-use kubeconfig as a YAML document (embeds the token — shown only at create time; re-download later via the download route)."
                        }
                      },
                      "required": [
                        "id",
                        "name",
                        "role",
                        "createdAt",
                        "expiresAt",
                        "status",
                        "kubeconfig"
                      ]
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/kubeconfigs — List long-lived kubeconfig credentials for a cluster",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "description": "Credential metadata. The token itself is NEVER returned by the list — re-download it explicitly.",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "role": {
                            "type": "string",
                            "enum": [
                              "admin",
                              "view"
                            ]
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "expiresAt": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true,
                            "description": "When the token expires; null when ttl is \"never\"."
                          },
                          "revokedAt": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "lastDownloadedAt": {
                            "type": "string",
                            "format": "date-time",
                            "nullable": true
                          },
                          "createdByAdmin": {
                            "type": "boolean",
                            "description": "True when RareCloud created this credential in order to operate the cluster on your behalf, rather than you creating it yourself. It is yours either way — it is listed here and you can revoke it at any time."
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "active",
                              "expired",
                              "revoking",
                              "revoked"
                            ]
                          }
                        },
                        "required": [
                          "id",
                          "name",
                          "role",
                          "createdAt",
                          "expiresAt",
                          "revokedAt",
                          "lastDownloadedAt",
                          "createdByAdmin",
                          "status"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/kubeconfigs/{credId}/download": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/kubeconfigs/{credId}/download — Re-download a long-lived kubeconfig (active credentials only)",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "kubeconfig": {
                          "type": "string",
                          "description": "The kubeconfig as a YAML document (embeds the credential token)."
                        },
                        "name": {
                          "type": "string"
                        },
                        "expiresAt": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "When the token expires; null when ttl is \"never\"."
                        }
                      },
                      "required": [
                        "kubeconfig",
                        "name",
                        "expiresAt"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "credId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/kubeconfigs/{credId}": {
      "delete": {
        "tags": [
          "Services"
        ],
        "summary": "DELETE /services/{id}/kubeconfigs/{credId} — Revoke a long-lived kubeconfig credential (deletes its ServiceAccount — the token dies instantly)",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "revoking",
                            "revoked"
                          ],
                          "description": "revoked = ServiceAccount deleted; revoking = queued for the reaper (cluster unreachable at revoke time)."
                        }
                      },
                      "required": [
                        "id",
                        "status"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "credId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/scale": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/scale — Current worker pools + add-ons for a managed Kubernetes cluster (your own only)",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "POST /services/{id}/scale — Set the first worker pool's min/max workers (your own cluster only)",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "minimum": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "Minimum worker count for the first pool."
                  },
                  "maximum": {
                    "type": "integer",
                    "description": "Maximum worker count (must be >= minimum)."
                  }
                },
                "required": [
                  "minimum",
                  "maximum"
                ]
              }
            }
          }
        }
      }
    },
    "/services/{id}/pools": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/pools — List the node pools of a managed Kubernetes cluster (your own only)",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "POST /services/{id}/pools — Add a named node pool to a managed Kubernetes cluster (your own only)",
        "responses": {
          "400": {
            "description": "INVALID_PARAM: a bad pool name or range, or a machine type that cannot run Kubernetes workers, for example a shared-CPU s-* plan (message: Machine type \"s-2vcpu-4gb\" is not available for managed Kubernetes worker pools. Choose a G, C or M plan.)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Node-pool name (lowercase letters/digits/hyphens, start with a letter, max 15; unique within the cluster)."
                  },
                  "machineType": {
                    "type": "string",
                    "description": "Worker machine type / flavor. Resolved server-side (catalog SKU or CloudProfile flavor name); a bogus value is rejected. Defaults to the cluster default when omitted. Only G, C and M plans (g-*, c-*, m-*: the plans the catalog marks usable_for k8s, listed by GET /catalog/listings/cloud-k8s) can run worker pools; a shared-CPU s-* plan is refused with 400 INVALID_PARAM before anything is reserved."
                  },
                  "minimum": {
                    "type": "integer",
                    "description": "Minimum worker count (autoscaling lower bound)."
                  },
                  "maximum": {
                    "type": "integer",
                    "description": "Maximum worker count (>= minimum, <= 16)."
                  },
                  "volumeSizeGb": {
                    "type": "integer",
                    "minimum": 10,
                    "maximum": 1000,
                    "description": "Per-node root-volume size in GiB. Defaults to 30 when omitted."
                  }
                },
                "required": [
                  "name",
                  "minimum",
                  "maximum"
                ]
              }
            }
          }
        }
      }
    },
    "/services/{id}/pools/{pool}": {
      "patch": {
        "tags": [
          "Services"
        ],
        "summary": "PATCH /services/{id}/pools/{pool} — Edit a node pool's min/max/machineType/volume (your own cluster only)",
        "responses": {
          "400": {
            "description": "INVALID_PARAM: a bad pool name or range, or a machine type that cannot run Kubernetes workers, for example a shared-CPU s-* plan (message: Machine type \"s-2vcpu-4gb\" is not available for managed Kubernetes worker pools. Choose a G, C or M plan.)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pool",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "minimum": {
                    "type": "integer",
                    "description": "New minimum worker count."
                  },
                  "maximum": {
                    "type": "integer",
                    "description": "New maximum worker count (>= minimum, <= 16)."
                  },
                  "machineType": {
                    "type": "string",
                    "description": "New worker machine type / flavor. Resolved server-side; a bogus value is rejected. Checked only when it differs from the pool's current type. Only G, C and M plans (g-*, c-*, m-*: the plans the catalog marks usable_for k8s, listed by GET /catalog/listings/cloud-k8s) can run worker pools; a shared-CPU s-* plan is refused with 400 INVALID_PARAM before anything is reserved."
                  },
                  "volumeSizeGb": {
                    "type": "integer",
                    "minimum": 10,
                    "maximum": 1000,
                    "description": "New per-node root-volume size in GiB."
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Services"
        ],
        "summary": "DELETE /services/{id}/pools/{pool} — Remove a node pool (never the last one; your own cluster only)",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pool",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/pools/{pool}/rename": {
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "POST /services/{id}/pools/{pool}/rename — Rename a node pool (add-new + remove-old → rolling node replacement; your own cluster only)",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pool",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "The new node-pool name (lowercase letters/digits/hyphens, start with a letter, max 15; unique within the cluster). The 64-character maxLength here is only the route's hard input cap — the actual pool-naming rule (max 15 characters) is enforced server-side by the service layer and returns 400 INVALID_PARAM if violated."
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        }
      }
    },
    "/services/{id}/resize": {
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "POST /services/{id}/resize — Resize a cloud VM to a different plan (async)",
        "description": "Resize (upgrade/downgrade) a cloud VM you own to a different plan. Pass the target plan's PUBLIC SKU (e.g. `c-4vcpu-8gb`) in `flavor` — NOT a raw OpenStack flavor id; the SKU is mapped to the backing flavor server-side. The resize is asynchronous: the VM cold-migrates and reboots, so it returns **202 Accepted** with `status:\"resizing\"` immediately, then settles to ACTIVE on the new plan within a couple of minutes (poll `GET /services/{id}`). Downgrading to a smaller disk is rejected (cloud VMs cannot shrink their root disk). Moving to a plan with a larger disk grows the root disk to the new plan's size before the resize starts, and the resize's reboot grows the filesystem; a disk that is already larger is kept as it is. If the disk cannot be grown, the resize is not started and the VM stays on its current plan (retry the same request). Billing automatically follows the new plan's hourly rate from the next meter tick. Requires scope `services:write`; you can only resize your own VM.",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "202": {
            "description": "Resize accepted — the VM is migrating; poll GET /services/{id} until it returns to ACTIVE.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "The VM (Nova server) id being resized."
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "resizing"
                          ]
                        },
                        "flavor": {
                          "type": "string",
                          "description": "The target plan's public SKU you requested."
                        }
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "400": {
            "description": "Unknown/non-cloud plan SKU, or a disallowed disk-shrink (INVALID_PARAM)."
          },
          "404": {
            "description": "No cloud VM with this id belongs to you (NOT_FOUND)."
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The cloud VM (Nova server) id."
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "flavor": {
                    "type": "string",
                    "description": "Target plan PUBLIC SKU (e.g. `c-4vcpu-8gb`). Not a raw OpenStack flavor id — it is mapped to the backing flavor server-side.",
                    "example": "c-4vcpu-8gb"
                  }
                },
                "required": [
                  "flavor"
                ]
              }
            }
          }
        }
      }
    },
    "/services/{id}/rdns": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "Get a Cloud VM's reverse DNS record",
        "description": "Returns the PTR record for the primary public IPv4 address of a Cloud VM you own. The IP is derived server-side and cannot be supplied by the caller. Requires scope `services:read`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "The current PTR record, or null hostname and TTL when none exists.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/ReverseDnsRecord"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No Cloud VM with this id belongs to you."
          }
        }
      },
      "put": {
        "tags": [
          "Services"
        ],
        "summary": "Set a Cloud VM's reverse DNS record",
        "description": "Replaces the PTR record for the primary public IPv4 address of a Cloud VM you own. The IP is derived server-side and must belong to a configured RareCloud reverse zone. Requires scope `services:write`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "hostname"
                ],
                "properties": {
                  "hostname": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 254,
                    "example": "mail.example.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "The normalized PTR record.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/ReverseDnsRecord"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid hostname or the VM's IPv4 is outside the managed reverse zones."
          },
          "404": {
            "description": "No Cloud VM with this id belongs to you."
          }
        }
      },
      "delete": {
        "tags": [
          "Services"
        ],
        "summary": "Remove a Cloud VM's reverse DNS record",
        "description": "Deletes the PTR record for the primary public IPv4 address of a Cloud VM you own. The IP is derived server-side. Requires scope `services:write`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "The now-empty reverse DNS state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "ok",
                    "data"
                  ],
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/ReverseDnsRecord"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No Cloud VM with this id belongs to you."
          }
        }
      }
    },
    "/services/{id}/high-availability": {
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "POST /services/{id}/high-availability — Enable the HA control plane on a managed Kubernetes cluster (add-only; can't be removed; your own cluster only)",
        "description": "Enables the high-availability control plane on a managed Kubernetes cluster you own. This is an add-only, idempotent operation — once enabled, the HA control plane cannot be removed (Gardener constraint). Enabling HA adds a €30/mo surcharge billed hourly from the next meter tick. No request body required. Ownership (IDOR) is enforced server-side; a cluster you don't own returns 404. Requires scope `services:write`.",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK — HA control plane enabled (or was already enabled). Returns the full cluster detail.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "404": {
            "description": "No managed Kubernetes cluster with this id belongs to you (NOT_FOUND)."
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The managed Kubernetes cluster (Gardener shoot) id."
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/hostname": {
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "POST /services/{id}/hostname: Change the hostname of a VPS or rename a cloud VM",
        "description": "Works for legacy VPS ids (numeric) and cloud VM UUIDs; returns the updated Service. For a legacy VPS it changes the hostname in the virtualization panel. For a cloud VM it renames the server (the name shown in the console and the API); it does NOT change the hostname inside an operating system that has already booted. Names starting with `shoot--`, `kube_service_` or `migrate-helper` are reserved and rejected (INVALID_PARAM), and a Kubernetes worker node cannot be renamed (CONFLICT). A cloud VM UUID you do not own returns NOT_FOUND, the same as one that does not exist. Requires the `services:write` scope.",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "hostname": {
                    "type": "string",
                    "maxLength": 253,
                    "description": "RFC-1123-style hostname (letters, digits, hyphens and dots)."
                  }
                },
                "required": [
                  "hostname"
                ]
              }
            }
          }
        }
      }
    },
    "/services/{id}/tags": {
      "put": {
        "tags": [
          "Services"
        ],
        "summary": "PUT /services/{id}/tags: Replace a cloud VM's tags",
        "description": "Replaces the whole tag set of a cloud VM (send `[]` to clear it) and returns the updated Service, whose `tags` then holds the new set. At most 50 tags, each 1 to 60 characters without `,` `/` or control characters; duplicates are dropped. Tags starting with `managed:`, `k8s:` or `rarecloud` (any case) are reserved for the platform and rejected (INVALID_PARAM). A VM that is still building or in an error state cannot change its tags yet (CONFLICT), and neither can a Kubernetes worker node (CONFLICT). A cloud VM UUID you do not own returns NOT_FOUND, the same as one that does not exist. A legacy service id returns NOT_IMPLEMENTED (tags are for cloud VMs only). Requires the `services:write` scope.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Cloud VM id (UUID)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tags": {
                    "type": "array",
                    "maxItems": 50,
                    "items": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 60
                    },
                    "description": "The complete new tag set."
                  }
                },
                "required": [
                  "tags"
                ]
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Service"
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/password": {
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "POST /services/{id}/password — Reset the VPS root password (min 8 chars; never logged)",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "password": {
                    "type": "string",
                    "minLength": 8,
                    "maxLength": 128
                  }
                },
                "required": [
                  "password"
                ]
              }
            }
          }
        }
      }
    },
    "/services/{id}/cancel": {
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "POST /services/{id}/cancel — File a cancellation request (billing stops; WHMCS terminates via module)",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "end_of_term",
                      "immediate"
                    ]
                  },
                  "reason": {
                    "type": "string",
                    "maxLength": 1000
                  }
                },
                "required": []
              }
            }
          }
        }
      }
    },
    "/services/{id}/upgrade": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/upgrade — Candidate plans+cycles for upgrade/downgrade",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "POST /services/{id}/upgrade — Prorated quote (confirm=false, default) or create the upgrade order+invoice (confirm=true)",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "newProductId": {
                    "type": "string",
                    "maxLength": 64
                  },
                  "cycle": {
                    "type": "string",
                    "enum": [
                      "monthly",
                      "quarterly",
                      "semiannually",
                      "annually",
                      "biennially",
                      "triennially",
                      "hourly"
                    ]
                  },
                  "confirm": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "newProductId",
                  "cycle"
                ]
              }
            }
          }
        }
      }
    },
    "/services/{id}/panel-sso": {
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "POST /services/{id}/panel-sso — Mint a single-use SSO URL into the full legacy management panel",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/cpanel-sso": {
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "POST /services/{id}/cpanel-sso — Mint a single-use SSO URL deep-linked into a specific cPanel app (hosting only)",
        "description": "Reuses the same CreateSsoToken WHMCS action as panel-sso, but adds dosinglesignon=1 (+ an optional app=) to the client-area path so WHMCS auto-logs the client straight into that cPanel section instead of the generic product page. `app` omitted logs into cPanel's own home. Only available for hosting (cPanel) services — a VPS/proxy id returns INVALID_PARAM.",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "app": {
                    "type": "string",
                    "enum": [
                      "Email_Accounts",
                      "FileManager_Home"
                    ],
                    "description": "Omit for plain cPanel-home login. Strict whitelist — any other value is rejected with INVALID_PARAM."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/services/{id}/renew-sso": {
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "POST /services/{id}/renew-sso — Mint a single-use SSO URL to the early-renewal page",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/tickets/{id}/close": {
      "post": {
        "tags": [
          "Tickets"
        ],
        "summary": "POST /tickets/{id}/close — Close a support ticket",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/billing/payment-methods": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "GET /billing/payment-methods — Available payment options (WHMCS gateways; no stored-card vault)",
        "description": "Requires scope `billing:read`. Returns your account's available payment options.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PaymentMethod"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "parameters": [],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "post": {
        "tags": [
          "Billing"
        ],
        "summary": "POST /billing/payment-methods — add a payment method to your own account",
        "description": "Requires scope `billing:write`. Adds a payment method to the authenticated account (the client is resolved from your auth context, never the body). WHMCS has no card-vault API, so this returns 501 NOT_IMPLEMENTED on the WHMCS backend — methods are chosen at checkout instead.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PaymentMethod"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created payment method",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/PaymentMethod"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "501": {
            "description": "Not supported by this backend",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/billing/payment-methods/{id}": {
      "delete": {
        "tags": [
          "Billing"
        ],
        "summary": "DELETE /billing/payment-methods/{id} — remove one of your own payment methods",
        "description": "Requires scope `billing:write`. Removes a payment method from the authenticated account; the removal is scoped to your client so you can only remove your own.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/billing/campaign": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "GET /billing/campaign — the currently-active credit campaign (or null)",
        "description": "Requires scope `billing:read`. Returns the active deposit-match campaign in public shape (feeds the 'double your credits' banner), or `{campaign:null}` when none is running.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "campaign": {
                          "oneOf": [
                            {
                              "$ref": "#/components/schemas/Campaign"
                            },
                            {
                              "type": "null"
                            }
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/billing/bonus": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "GET /billing/bonus — promo (bonus) balance for the signed-in user",
        "description": "Requires scope `billing:read`. Returns the current promo balance in cents (EUR). The bonus balance is a SEPARATE non-WHMCS promo balance; it depletes first as a taxed discount line at consumption.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "balanceCents": {
                          "type": "integer",
                          "description": "Promo balance in the client's currency, cents (>= 0)."
                        },
                        "currency": {
                          "type": "string",
                          "enum": [
                            "EUR",
                            "USD"
                          ]
                        }
                      },
                      "required": [
                        "balanceCents",
                        "currency"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/billing/bonus/ledger": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "GET /billing/bonus/ledger — full bonus credit ledger (grants + consumption)",
        "description": "Requires scope `billing:read`. Returns all bonus ledger entries for the signed-in user (newest first): campaign grants (positive) and promo consumption (negative). Scoped to the authenticated user — no userId parameter.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "'grant-<n>' or 'consumption-<n>'"
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "grant",
                              "consumption"
                            ]
                          },
                          "amountCents": {
                            "type": "integer",
                            "description": "Positive for grants, negative for consumption (client currency)."
                          },
                          "currency": {
                            "type": "string",
                            "enum": [
                              "EUR",
                              "USD"
                            ]
                          },
                          "campaignName": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "whmcsInvoiceId": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "expiresAt": {
                            "type": [
                              "string",
                              "null"
                            ],
                            "format": "date-time"
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time"
                          }
                        },
                        "required": [
                          "id",
                          "type",
                          "amountCents",
                          "currency",
                          "campaignName",
                          "whmcsInvoiceId",
                          "expiresAt",
                          "createdAt"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/billing/invoices/{id}/checkout-url": {
      "post": {
        "tags": [
          "Billing"
        ],
        "summary": "POST /billing/invoices/{id}/checkout-url — logged-in hosted-gateway pay link",
        "description": "Requires scope `billing:write`. Returns a single-use, already-logged-in URL to the hosted invoice payment page (for card/PayPal/crypto that can't run in-dashboard). The invoice must belong to the authenticated account — others return 404 NOT_FOUND.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The hosted payment URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "url": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "url"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Invoice not found (or not yours)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/account/activity": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "GET /account/activity — your account audit trail (sign-ins, 2FA, service actions, billing ops)",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          },
          {
            "name": "before",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Last event id from the previous page (cursor)."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/billing/alert": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "GET /billing/alert: your billing alert (spend threshold, runway threshold in days, month-to-date spend, runway, triggered)",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/BillingAlertState"
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "put": {
        "tags": [
          "Billing"
        ],
        "summary": "PUT /billing/alert: set the spend threshold, the runway threshold in days, or both",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "thresholdCents": {
                    "type": "integer",
                    "minimum": 100,
                    "nullable": true,
                    "description": "Spend threshold in EUR cents (at least 100). Omit to keep the stored value, null to clear it"
                  },
                  "thresholdDays": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 60,
                    "nullable": true,
                    "description": "Runway threshold in days (1 to 60): you are emailed once a day while your credit has fewer days of runway left than this. Omit to keep the stored value, null to clear it"
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "Turns the alert on or off. Omitted keeps the stored value, except on a body that sets a threshold: setting a threshold arms the alert (enabled defaults to true), so a client sending only thresholdCents re-enables a disabled alert"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/BillingAlertState"
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "delete": {
        "tags": [
          "Billing"
        ],
        "summary": "DELETE /billing/alert: remove the billing alert",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean",
                          "enum": [
                            true
                          ],
                          "description": "The alert is gone. Nothing is left to report about it."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/account/emails": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "GET /account/emails — emails WHMCS sent you (list; ?id= returns one with HTML body)",
        "parameters": [
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/account/contacts": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "GET /account/contacts — billing/tech contacts (email-copy recipients, no login)",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "post": {
        "tags": [
          "Account"
        ],
        "summary": "POST /account/contacts — add/update/delete a contact ({ action, id?, firstname, lastname, email, ... })",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "add",
                      "update",
                      "delete"
                    ]
                  },
                  "id": {
                    "type": "integer",
                    "description": "Required (positive) for action=update|delete."
                  },
                  "firstname": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "Required for action=add."
                  },
                  "lastname": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "Required for action=add."
                  },
                  "email": {
                    "type": "string",
                    "maxLength": 254,
                    "description": "Required for action=add."
                  },
                  "phonenumber": {
                    "type": "string",
                    "maxLength": 32
                  },
                  "companyname": {
                    "type": "string",
                    "maxLength": 128
                  },
                  "generalemails": {
                    "type": "boolean"
                  },
                  "invoiceemails": {
                    "type": "boolean"
                  },
                  "supportemails": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "action"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/limits": {
      "get": {
        "tags": [
          "Limits"
        ],
        "summary": "GET /limits \u2014 Your effective quota limits and live usage",
        "description": "Effective limits are the baseline tier raised by any increase support has granted you. Usage is read live from the region. A resource whose service cannot be reached is returned with unavailable=true rather than zero usage \u2014 unknown is not the same as unused.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [true]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "resources": {
                          "type": "array",
                          "items": { "$ref": "#/components/schemas/QuotaLimitRow" }
                        },
                        "nearingLimit": {
                          "type": "boolean",
                          "description": "True when any readable resource is at or past 80% of its limit."
                        }
                      },
                      "required": [
                        "resources",
                        "nearingLimit"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/limits/estimate": {
      "post": {
        "tags": [
          "Limits"
        ],
        "summary": "POST /limits/estimate — What a managed-Kubernetes deployment would consume",
        "description": "Answers two questions about a set of worker pools before anything is ordered: whether the pools' MINIMUMS fit in what is left of your quota (blocking — a deploy cannot start without them), and how far each pool can actually scale toward the maximum you configured (advisory — autoscaling simply stops there). Nothing is created or reserved. available=false means a machine type has no catalog plan and no estimate could be made; treat it as 'unknown', not as 'does not fit'.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "pools": {
                    "type": "array",
                    "maxItems": 5,
                    "items": {
                      "type": "object",
                      "properties": {
                        "name": { "type": "string", "maxLength": 63 },
                        "machineType": { "type": "string", "description": "CloudProfile machine type, e.g. g-8vcpu-32gb." },
                        "minimum": { "type": "integer", "minimum": 1, "maximum": 16 },
                        "maximum": { "type": "integer", "minimum": 1, "maximum": 16 },
                        "volumeSizeGb": { "type": "integer", "minimum": 1, "description": "Root-disk override. Omit to use the machine type's own disk." }
                      },
                      "required": [
                        "machineType",
                        "minimum",
                        "maximum"
                      ]
                    }
                  }
                },
                "required": [
                  "pools"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [true]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "available": {
                          "type": "boolean",
                          "description": "False when no estimate could be made. Every other field is absent."
                        },
                        "blocking": {
                          "type": "object",
                          "additionalProperties": { "type": "integer" },
                          "description": "Consumption at the pools' minimums."
                        },
                        "ceiling": {
                          "type": "object",
                          "additionalProperties": { "type": "integer" },
                          "description": "Consumption with every pool fully scaled out."
                        },
                        "fits": {
                          "type": "boolean",
                          "description": "Whether `blocking` fits in the room left."
                        },
                        "shortfall": {
                          "type": "object",
                          "additionalProperties": { "type": "integer" },
                          "description": "How much of each resource is missing. Empty when fits=true; its keys are exactly what a quota-increase request should name."
                        },
                        "pools": {
                          "type": "array",
                          "items": { "$ref": "#/components/schemas/PoolCeiling" }
                        }
                      },
                      "required": [
                        "available"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/limits/requests": {
      "get": {
        "tags": ["Limits"],
        "summary": "GET /limits/requests \u2014 Your quota-increase requests, newest first",
        "responses": {
          "200": {
            "description": "OK",
            "content": { "application/json": { "schema": {
              "type": "object",
              "properties": {
                "ok": { "type": "boolean", "enum": [true] },
                "data": { "type": "object", "properties": {
                  "requests": { "type": "array", "items": { "$ref": "#/components/schemas/QuotaRequest" } }
                }, "required": ["requests"] }
              },
              "required": ["ok", "data"]
            } } }
          }
        },
        "security": [{ "bearerAuth": [] }, { "cookieAuth": [] }]
      },
      "post": {
        "tags": [
          "Limits"
        ],
        "summary": "POST /limits/requests \u2014 Ask for a quota increase",
        "description": "Modest increases are applied immediately (status auto_approved) when the region has headroom. Anything larger is left pending and opens a support ticket. Only one pending request exists at a time \u2014 submitting again replaces it.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "requested": {
                    "type": "object",
                    "description": "New ceiling per resource. Must exceed the current limit. -1 requests unlimited.",
                    "additionalProperties": {
                      "type": "integer"
                    }
                  },
                  "reason": {
                    "type": "string",
                    "minLength": 10,
                    "maxLength": 2000
                  },
                  "context": {
                    "type": "object",
                    "description": "Optional: what prompted this, e.g. {kind:'k8s', name:'prod'}."
                  }
                },
                "required": [
                  "requested",
                  "reason"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Request recorded (check status: auto_approved means it is already in effect)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/QuotaRequest"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "400": {
            "description": "No resources named, reason too short, or the value is at or below your current limit"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/reserved-ips": {
      "get": {
        "tags": [
          "Reserved IPs"
        ],
        "summary": "GET /reserved-ips — List your reserved (floating) IPs with their live attachment",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ReservedIp"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "post": {
        "tags": [
          "Reserved IPs"
        ],
        "summary": "POST /reserved-ips — Reserve a new public IP (€2/mo). Pass serverId to reserve AND attach in one call.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/ReservedIp"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "serverId": {
                    "type": "string",
                    "description": "Optional cloud VM id to attach the new IP to immediately."
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/reserved-ips/{id}": {
      "delete": {
        "tags": [
          "Reserved IPs"
        ],
        "summary": "DELETE /reserved-ips/{id} — Release a reserved IP (FIP deleted, billing stops). NOT reversible.",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {}
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/reserved-ips/{id}/attach": {
      "post": {
        "tags": [
          "Reserved IPs"
        ],
        "summary": "POST /reserved-ips/{id}/attach — Attach a reserved IP to one of your cloud VMs",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/ReservedIp"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "serverId": {
                    "type": "string",
                    "description": "The cloud VM id to attach to (must be owned by you)."
                  }
                },
                "required": [
                  "serverId"
                ]
              }
            }
          }
        }
      }
    },
    "/reserved-ips/{id}/detach": {
      "post": {
        "tags": [
          "Reserved IPs"
        ],
        "summary": "POST /reserved-ips/{id}/detach — Detach a reserved IP from its VM (stays allocated + billed until released)",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/ReservedIp"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/firewalls": {
      "get": {
        "tags": [
          "Firewalls"
        ],
        "summary": "GET /firewalls — List your cloud firewalls (security groups)",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Firewall"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "post": {
        "tags": [
          "Firewalls"
        ],
        "summary": "POST /firewalls — Create a new firewall",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 63
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/FirewallDetail"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/firewalls/{id}": {
      "get": {
        "tags": [
          "Firewalls"
        ],
        "summary": "GET /firewalls/{id} — Get firewall detail including rules",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/FirewallDetail"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "delete": {
        "tags": [
          "Firewalls"
        ],
        "summary": "DELETE /firewalls/{id} — Delete a firewall (must be detached from all VMs first)",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/firewalls/{id}/rules": {
      "post": {
        "tags": [
          "Firewalls"
        ],
        "summary": "POST /firewalls/{id}/rules — Add a rule to a firewall",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FirewallRuleInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/FirewallDetail"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/firewalls/{id}/rules/{ruleId}": {
      "delete": {
        "tags": [
          "Firewalls"
        ],
        "summary": "DELETE /firewalls/{id}/rules/{ruleId} — Remove a rule from a firewall",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "ruleId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/FirewallDetail"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/firewalls/{id}/attach": {
      "post": {
        "tags": [
          "Firewalls"
        ],
        "summary": "POST /firewalls/{id}/attach — Attach a firewall to a cloud VM",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "serverId": {
                    "type": "string",
                    "description": "The cloud VM id to attach the firewall to."
                  }
                },
                "required": [
                  "serverId"
                ]
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/firewalls/{id}/detach": {
      "post": {
        "tags": [
          "Firewalls"
        ],
        "summary": "POST /firewalls/{id}/detach — Detach a firewall from a cloud VM",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "serverId": {
                    "type": "string",
                    "description": "The cloud VM id to detach the firewall from."
                  }
                },
                "required": [
                  "serverId"
                ]
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/snapshots": {
      "get": {
        "tags": ["Snapshots"],
        "summary": "List your snapshots",
        "description": "Every snapshot you own, across all of your cloud VMs.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "enum": [true] },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Snapshot" } }
                  },
                  "required": ["ok", "data"]
                }
              }
            }
          }
        }
      }
    },
    "/snapshots/{id}": {
      "get": {
        "tags": ["Snapshots"],
        "summary": "Get a snapshot",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Snapshot id.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "enum": [true] },
                    "data": { "$ref": "#/components/schemas/Snapshot" }
                  },
                  "required": ["ok", "data"]
                }
              }
            }
          },
          "404": { "description": "Not found / not yours" }
        }
      },
      "delete": {
        "tags": ["Snapshots"],
        "summary": "Delete a snapshot",
        "description": "Never blocked by a snapshot still being secured — this is how you release a source disk if one gets stuck. It IS blocked while VMs created from this snapshot still exist, and the error names them.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Snapshot id.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "enum": [true] },
                    "data": { "type": "object", "properties": { "ok": { "type": "boolean" } } }
                  },
                  "required": ["ok", "data"]
                }
              }
            }
          },
          "404": { "description": "Not found / not yours" },
          "409": { "description": "VMs or volumes created from this snapshot still depend on it" }
        }
      }
    },
    "/services/{id}/snapshots": {
      "get": {
        "tags": ["Snapshots"],
        "summary": "List a cloud VM's snapshots",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Cloud VM id. Snapshots are a cloud-VM feature; any other service id returns 404.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "enum": [true] },
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Snapshot" } }
                  },
                  "required": ["ok", "data"]
                }
              }
            }
          },
          "404": { "description": "Not a cloud VM" }
        }
      },
      "post": {
        "tags": [
          "Snapshots"
        ],
        "summary": "Take a snapshot of a cloud VM",
        "description": "Returns as soon as the snapshot is recorded. It becomes usable within seconds and reaches ready once fully independent. The VM keeps running throughout. Up to 5 snapshots per VM.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Cloud VM id. Snapshots are a cloud-VM feature; any other service id returns 404.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 64
                  }
                },
                "required": [
                  "name"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Snapshot"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "400": {
            "description": "Invalid body"
          },
          "404": {
            "description": "Not a cloud VM"
          },
          "409": {
            "description": "Snapshot limit reached for this VM, or the VM has no identifiable boot disk. Also IDEMPOTENCY_IN_PROGRESS: a request with the same Idempotency-Key is still running; retry after Retry-After seconds with the same key."
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        }
      }
    },
    "/services/{id}/restore": {
      "post": {
        "tags": ["Snapshots"],
        "summary": "Restore a cloud VM from one of its snapshots",
        "description": "DESTRUCTIVE. The VM's disk is replaced by the snapshot's contents and everything written since is lost. The snapshot must be one taken from this same VM.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Cloud VM id. Snapshots are a cloud-VM feature; any other service id returns 404.",
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": { "snapshotId": { "type": "string", "format": "uuid" } },
                "required": ["snapshotId"],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "enum": [true] },
                    "data": { "type": "object", "properties": { "status": { "type": "string", "enum": ["restoring"] }, "serverId": { "type": "string", "format": "uuid" } } }
                  },
                  "required": ["ok", "data"]
                }
              }
            }
          },
          "400": { "description": "Invalid body, or the snapshot was taken from a different VM" },
          "404": { "description": "Not a cloud VM, or snapshot not found" },
          "409": { "description": "A snapshot of this VM is still being secured" }
        }
      }
    },
    "/volumes": {
      "get": {
        "tags": [
          "Volumes"
        ],
        "summary": "List block volumes",
        "description": "Every Cinder block volume the authenticated user owns, scoped to their OpenStack project. Requires scope services:read.",
        "responses": {
          "503": {
            "$ref": "#/components/responses/BackendUnavailable"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Service"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Volumes"
        ],
        "summary": "Create a block volume",
        "description": "Create a Cinder block volume in the caller's project. Per-GB priced (not a catalog SKU). Requires scope services:write.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "sizeGb": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 2048,
                    "description": "Volume size in GB."
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 253,
                    "description": "Optional display name (default 'volume')."
                  }
                },
                "required": [
                  "sizeGb"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Service"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/volumes/{id}": {
      "get": {
        "tags": [
          "Volumes"
        ],
        "summary": "Get a block volume",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "503": {
            "$ref": "#/components/responses/BackendUnavailable"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Service"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found / not yours"
          }
        }
      },
      "delete": {
        "tags": [
          "Volumes"
        ],
        "summary": "Delete a block volume",
        "description": "Deletes a volume you own. A volume managed by one of your Kubernetes clusters (a PersistentVolumeClaim, tagged `managed:kubernetes`) is refused with 409 — delete it from inside the cluster. The single exception is an ORPHAN (also tagged `k8s:orphaned`): its cluster no longer exists, so nothing else can delete it while it keeps being billed. An orphan may be deleted here by echoing its exact name in the `confirm` query parameter.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "confirm",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Typed confirmation for deleting an ORPHANED Kubernetes-managed volume: the volume's exact name. Required only for that case, ignored otherwise. Anything other than an exact match is refused with 409 — the data on the volume cannot be recovered afterwards."
          }
        ],
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean"
                        }
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "409": {
            "description": "The volume is managed by one of your Kubernetes clusters (delete it from the cluster), or it is an orphan and `confirm` did not match its exact name"
          }
        }
      }
    },
    "/volumes/{id}/attach": {
      "post": {
        "tags": [
          "Volumes"
        ],
        "summary": "Attach a volume to a VM",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "serverId": {
                    "type": "string",
                    "description": "Nova server id to attach to (must be in your project)."
                  }
                },
                "required": [
                  "serverId"
                ]
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "attachmentId": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/volumes/{id}/detach": {
      "post": {
        "tags": [
          "Volumes"
        ],
        "summary": "Detach a volume from a VM",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "serverId": {
                    "type": "string",
                    "description": "Nova server id to detach from."
                  }
                },
                "required": [
                  "serverId"
                ]
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean"
                        }
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/load-balancers": {
      "get": {
        "tags": [
          "Load balancers"
        ],
        "summary": "List load balancers",
        "description": "Every L4 (TCP) load balancer the caller owns, project-scoped. Requires scope services:read.",
        "responses": {
          "503": {
            "$ref": "#/components/responses/BackendUnavailable"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Service"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Load balancers"
        ],
        "summary": "Create an L4 load balancer",
        "description": "Create a TCP load balancer: VIP on the customer subnet, listener+pool on `port`, the chosen VMs as members, and a public floating IP. Requires per-customer tenancy. Requires scope services:write.\n\nAsynchronous: the call returns as soon as the load balancer exists, with its `id` and `status: \"pending\"`. The listener, pool, members, health check and public IP are then built in the background, which usually takes a few minutes. Poll `GET /load-balancers/{id}` until `status` is `active`. If the build gives up, `status` becomes `error` with a one-line `statusReason`; the load balancer is not deleted automatically (delete it yourself). Send an `Idempotency-Key` so a retried create returns the same load balancer instead of building a second one.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 253
                  },
                  "port": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 65535,
                    "description": "Listener + member port."
                  },
                  "memberServerIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 1,
                    "description": "Nova server ids to balance across."
                  },
                  "healthCheck": {
                    "type": "boolean",
                    "description": "TCP health monitor (default true)."
                  }
                },
                "required": [
                  "name",
                  "port",
                  "memberServerIds"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Service"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/load-balancers/{id}": {
      "get": {
        "tags": [
          "Load balancers"
        ],
        "summary": "Get a load balancer",
        "description": "`status` is `pending` while the load balancer is still being built (a `build:<step>` tag names the step: listener, pool, members, health_monitor, floating_ip), `active` once it is ready, and `error` when the build gave up (`statusReason` says why). Members and the public IP appear as they are created. After a delete that was accepted in the background, `status` is `deleting` until the load balancer is gone (then this call answers 404); a deletion that failed reads `error` with `statusReason` `Deletion failed, contact support`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "503": {
            "$ref": "#/components/responses/BackendUnavailable"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Service"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found / not yours"
          }
        }
      },
      "delete": {
        "tags": [
          "Load balancers"
        ],
        "summary": "Delete a load balancer",
        "description": "Cascade-deletes the listener/pool/members/HM and releases the VIP floating IP. k8s-managed LBs are refused (manage via the Kubernetes Service). Requires scope services:write.\n\nA load balancer that is ready (`active`) is deleted in the call, which answers `{ \"ok\": true, \"status\": \"deleted\" }`. A load balancer that is still being built, or that is still applying a change, is accepted at once: the build is stopped, the call answers 200 `{ \"ok\": true, \"status\": \"deleting\" }` within seconds, and the load balancer is deleted in the background as soon as it can be (its public IP is released too). It then reads `status: \"deleting\"` until it is gone, after which `GET /load-balancers/{id}` answers 404. Deleting it again while it is deleting answers the same 200. If the background deletion fails, the load balancer reads `status: \"error\"` with `statusReason` `Deletion failed, contact support`. To wait for the deletion, poll `GET /load-balancers/{id}` until 404.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "deleted",
                            "deleting"
                          ],
                          "description": "`deleted`: the load balancer is gone. `deleting`: accepted; it is deleted in the background (poll GET /load-balancers/{id} until 404). Added 2026-10-08; older clients that read only `ok` keep working."
                        }
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/load-balancers/{id}/members": {
      "get": {
        "tags": [
          "Load balancers"
        ],
        "summary": "List load balancer members",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LbMember"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Load balancers"
        ],
        "summary": "Add a member",
        "description": "Add a VM (by its private fixed IP) to the LB pool on the given port. Requires scope services:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "serverId": {
                    "type": "string",
                    "description": "Nova server id to add (must be in your project)."
                  },
                  "port": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 65535
                  }
                },
                "required": [
                  "serverId",
                  "port"
                ]
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        }
      }
    },
    "/load-balancers/{id}/members/{mid}": {
      "delete": {
        "tags": [
          "Load balancers"
        ],
        "summary": "Remove a member",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "mid",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean"
                        }
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/networks": {
      "get": {
        "tags": [
          "Networks"
        ],
        "summary": "List VPCs",
        "description": "Every VPC (virtual network) the caller owns. Requires scope services:read.",
        "responses": {
          "503": {
            "$ref": "#/components/responses/BackendUnavailable"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Vpc"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Networks"
        ],
        "summary": "Create a VPC",
        "description": "Create a VPC by name; the CIDR is auto-allocated (10.N.0.0/16). Max 5 per account. Requires scope services:write.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 253
                  }
                },
                "required": [
                  "name"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Vpc"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/networks/{id}": {
      "get": {
        "tags": [
          "Networks"
        ],
        "summary": "Get a VPC",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "503": {
            "$ref": "#/components/responses/BackendUnavailable"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Vpc"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "404": {
            "description": "Not found / not yours"
          }
        }
      },
      "delete": {
        "tags": [
          "Networks"
        ],
        "summary": "Delete a VPC",
        "description": "Delete a VPC (refused for the default VPC or one that still has VMs attached). Requires scope services:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean"
                        }
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/networks/{id}/vms": {
      "post": {
        "tags": [
          "Networks"
        ],
        "summary": "Move a VM into this VPC",
        "description": "Attach the given VM to this VPC (moves it off its current VPC; the VM's public eth0 is untouched). Requires scope services:write.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "description": "Target VPC (network) id."
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "serverId": {
                    "type": "string",
                    "description": "Nova server id to move."
                  }
                },
                "required": [
                  "serverId"
                ]
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean"
                        }
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/billing/state": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "GET /billing/state: the open cloud invoice, the dated power-off and deletion schedule, and the informational runway for the signed-in user",
        "description": "Requires scope `billing:read`. Read-only.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "state": {
                          "type": "string",
                          "enum": [
                            "ok",
                            "grace",
                            "suspended"
                          ],
                          "description": "`ok` or `suspended`. `grace` is retained in the enum for clients built against the pre-2026-09-22 billing model and is never returned any more: there is no grace period, and an account is suspended once an unpaid cloud invoice has run out its 7 day payment window"
                        },
                        "graceEndsAt": {
                          "type": "string",
                          "nullable": true,
                          "description": "Always null. There is no grace clock in the current billing model; the field is kept so clients built against the older one keep parsing the response. Use openInvoice.dueAt and suspendAt"
                        },
                        "availableCents": {
                          "type": "integer",
                          "nullable": true,
                          "description": "INFORMATIONAL. Spendable balance in `currency` cents; null when the billing backend could not be read (unknown, which is not the same as zero). It enforces nothing: credit settles cloud invoices automatically, and what suspends an account is an unpaid invoice, not a low balance"
                        },
                        "suspendedCount": {
                          "type": "integer",
                          "description": "How many of your cloud resources are currently suspended (every type, not only the deletable ones)"
                        },
                        "currency": {
                          "type": "string",
                          "nullable": true,
                          "description": "Your billing currency, e.g. EUR; the currency of availableCents, accruedMonthCents and burnCentsPerHour"
                        },
                        "accruedMonthCents": {
                          "type": "integer",
                          "nullable": true,
                          "description": "INFORMATIONAL. This calendar month's cloud usage accrued so far and not yet invoiced, in `currency` cents"
                        },
                        "burnCentsPerHour": {
                          "type": "number",
                          "nullable": true,
                          "description": "INFORMATIONAL. What your running resources cost per hour, in `currency` cents, fractional (a sub-cent hourly rate is a real rate and is not rounded away); 0 when nothing hourly is metered"
                        },
                        "runwayHours": {
                          "type": "number",
                          "nullable": true,
                          "description": "INFORMATIONAL. Hours your balance lasts at that burn; null when there is no burn or no balance to divide. A runway running out suspends nothing"
                        },
                        "suspendAt": {
                          "type": "string",
                          "nullable": true,
                          "description": "ISO; when your cloud resources are powered off because openInvoice is unpaid: 7 days after its due date, pushed into your own 09:00-20:00 local window. The invoice is due the day it is issued, so this is roughly a week after dueAt, not the day after it. THIS is the deadline to act on. Null when nothing of ours is unpaid. On an already suspended account it is the moment the power-off was scheduled for, which is in the past"
                        },
                        "deleteAt": {
                          "type": "string",
                          "nullable": true,
                          "description": "ISO; when the earliest suspended resource is permanently deleted, in the same local window. Null when nothing is scheduled or an operator hold is on the account"
                        },
                        "openInvoice": {
                          "type": "object",
                          "nullable": true,
                          "description": "The oldest UNPAID invoice we issued for your cloud usage, or null when you owe nothing (also null when the invoice could not be read: unknown, not the same as nothing owed). This invoice is what the suspension calendar runs on. A VPS, proxy, hosting or domain renewal is never reported here and never powers off a cloud resource. Pay it, or hold enough credit for it to be settled automatically, and everything resumes within the hour",
                          "properties": {
                            "id": {
                              "type": "integer",
                              "description": "The invoice number, the same one shown at GET /billing/invoices/{id}"
                            },
                            "amountCents": {
                              "type": "integer",
                              "nullable": true,
                              "description": "The invoice total in `openInvoice.currency` cents; null when the billing backend gave no usable figure (the amount is then not stated rather than stated wrongly)"
                            },
                            "currency": {
                              "type": "string",
                              "nullable": true,
                              "description": "The INVOICE's own currency, which is not necessarily the account currency of the fields above"
                            },
                            "dueAt": {
                              "type": "string",
                              "description": "ISO; midnight UTC of the due DAY printed on the invoice, which is the day it was issued: a cloud usage invoice bills service you have already had, so there is nothing to wait for before asking. This is where your payment window STARTS, not where anything happens. You then have 7 days, and only at the end of that week are your cloud resources powered off (see suspendAt for the exact instant)"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "parameters": [],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/domains/availability": {
      "get": {
        "tags": [
          "Domains"
        ],
        "summary": "Check domain availability",
        "description": "Requires scope `domains:read`. Pre-purchase WHOIS availability lookup for a single domain.",
        "parameters": [
          {
            "name": "domain",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The domain to check, e.g. example.com."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/DomainAvailability"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/domains/tld-pricing": {
      "get": {
        "tags": [
          "Domains"
        ],
        "summary": "List TLD pricing",
        "description": "Requires scope `domains:read`. Register/transfer/renew prices per TLD, in the account currency.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TldPrice"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/domains": {
      "get": {
        "tags": [
          "Domains"
        ],
        "summary": "List your domains",
        "description": "Requires scope `domains:read`. The authenticated account's domains.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Domain"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "post": {
        "tags": [
          "Domains"
        ],
        "summary": "Register a domain",
        "description": "Requires scope `domains:write`. Places a register ORDER (WHMCS has no 'register now' API) + invoice; the registrar fulfils on accept/payment. Returns status 'Pending'.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/DomainOrderResult"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domain": {
                    "type": "string",
                    "description": "The domain to register, e.g. example.com."
                  },
                  "years": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 10,
                    "default": 1
                  },
                  "nameservers": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 5
                  },
                  "idProtection": {
                    "type": "boolean"
                  },
                  "dnsManagement": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "domain"
                ]
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/domains/transfers": {
      "post": {
        "tags": [
          "Domains"
        ],
        "summary": "Transfer a domain in",
        "description": "Requires scope `domains:write`. Places a transfer-in ORDER (AddOrder domaintype=transfer) with the EPP/auth code. Returns status 'Pending'.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/DomainOrderResult"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domain": {
                    "type": "string"
                  },
                  "epp": {
                    "type": "string",
                    "description": "EPP / auth code from the losing registrar."
                  },
                  "years": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 10,
                    "default": 1
                  },
                  "nameservers": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 5
                  },
                  "idProtection": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "domain",
                  "epp"
                ]
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/domains/{id}": {
      "get": {
        "tags": [
          "Domains"
        ],
        "summary": "Get a domain",
        "description": "Requires scope `domains:read`. One owned domain (IDOR-guarded to the caller).",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Domain"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/domains/{id}/api-access": {
      "put": {
        "tags": [
          "Domains"
        ],
        "summary": "Set API access for a domain (console only)",
        "description": "Turn API access for one domain on (`full`) or off (`read_only`). When off, API tokens and agents can still read the domain but cannot change its nameservers, DNS, contacts, lock/auto-renew/ID protection or renew it. **Console session only**: an API token gets 403 `FORBIDDEN`. Another account's id is 404. Audited as `resource.api_access`. Returns the updated Domain.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "apiAccess": {
                    "type": "string",
                    "enum": [
                      "full",
                      "read_only"
                    ],
                    "description": "`read_only` makes the resource read-only for API tokens and agents; `full` turns API access back on."
                  }
                },
                "required": [
                  "apiAccess"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Domain"
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid body, or a resource kind without the switch yet (object storage).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Called with an API token. Error code `FORBIDDEN`, message \"API access can only be changed in the console.\"",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/domains/{id}/renew": {
      "post": {
        "tags": [
          "Domains"
        ],
        "summary": "Renew a domain",
        "description": "Requires scope `domains:write`. Creates a CHARGED renewal order + invoice for an owned domain (paid from credit or via the invoice flow, like register/transfer) and optionally flips auto-renew. The renewal is fulfilled by the registrar once the order is paid; status is `Pending` until then.",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "orderId": {
                          "type": "string"
                        },
                        "domainId": {
                          "type": "string"
                        },
                        "invoiceId": {
                          "type": ["string", "null"]
                        },
                        "years": {
                          "type": "integer"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "Pending"
                          ]
                        }
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "years": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 10,
                    "default": 1
                  },
                  "autoRenew": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/domains/{id}/nameservers": {
      "get": {
        "tags": [
          "Domains"
        ],
        "summary": "Get nameservers",
        "description": "Requires scope `domains:read`. The owned domain's nameservers.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "nameservers": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "nameservers"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "put": {
        "tags": [
          "Domains"
        ],
        "summary": "Set nameservers",
        "description": "Requires scope `domains:write`. Replace the owned domain's nameservers (2–5).",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "nameservers": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      },
                      "required": [
                        "nameservers"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "nameservers": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 2,
                    "maxItems": 5
                  }
                },
                "required": [
                  "nameservers"
                ]
              }
            }
          }
        }
      }
    },
    "/domains/{id}/contacts": {
      "get": {
        "tags": [
          "Domains"
        ],
        "summary": "Get registrant contact",
        "description": "Requires scope `domains:read`. The owned domain's registrant WHOIS contact (registrar-dependent).",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "contact": {
                          "$ref": "#/components/schemas/DomainContact"
                        }
                      },
                      "required": [
                        "contact"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "put": {
        "tags": [
          "Domains"
        ],
        "summary": "Update registrant contact",
        "description": "Requires scope `domains:write`. Update the owned domain's registrant WHOIS contact (registrar-dependent).",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean"
                        }
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DomainContact"
              }
            }
          }
        }
      }
    },
    "/domains/{id}/dns": {
      "get": {
        "tags": [
          "Domains"
        ],
        "summary": "Get DNS host records",
        "description": "Requires scope `domains:read`. The owned domain's DNS host records. Registrar-dependent — returns 501 NOT_IMPLEMENTED when the registrar has no DNS API.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "records": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/DnsRecord"
                          }
                        }
                      },
                      "required": [
                        "records"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "put": {
        "tags": [
          "Domains"
        ],
        "summary": "Set DNS host records",
        "description": "Requires scope `domains:write`. Replace the owned domain's DNS host records. Registrar-dependent — returns 501 NOT_IMPLEMENTED when the registrar has no DNS API.",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "records": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/DnsRecord"
                          }
                        }
                      },
                      "required": [
                        "records"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "records": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/DnsRecord"
                    }
                  }
                },
                "required": [
                  "records"
                ]
              }
            }
          }
        }
      }
    },
    "/domains/{id}/manage": {
      "get": {
        "tags": [
          "Domains"
        ],
        "summary": "Get domain management snapshot",
        "description": "Requires scope `domains:read`. Combined management state (status, expiry, auto-renew, ID protection, nameservers, transfer lock) for an owned domain. Per-registrar fields (`nameservers`, `locked`) are null when the registrar exposes no API for them.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "domain": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string"
                        },
                        "registrationDate": {
                          "type": "string",
                          "nullable": true
                        },
                        "expiryDate": {
                          "type": "string",
                          "nullable": true
                        },
                        "autoRenew": {
                          "type": "boolean"
                        },
                        "idProtection": {
                          "type": "boolean"
                        },
                        "nameservers": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "nullable": true
                        },
                        "locked": {
                          "type": "boolean",
                          "nullable": true
                        }
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "post": {
        "tags": [
          "Domains"
        ],
        "summary": "Dispatch a domain management action",
        "description": "Requires scope `domains:write`. Single-action dispatch: `nameservers` (replace 2-5 NS), `lock` (transfer lock on/off), `autorenew` (on/off), `idprotect` (WHOIS privacy on/off), `epp` (email the transfer/EPP code).",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "ok": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        },
                        "message": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "ok"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "data"
                  ]
                }
              }
            }
          }
        },
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "action": {
                    "type": "string",
                    "enum": [
                      "nameservers",
                      "lock",
                      "autorenew",
                      "idprotect",
                      "epp"
                    ]
                  },
                  "nameservers": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "minItems": 2,
                    "maxItems": 5
                  },
                  "enabled": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "action"
                ]
              }
            }
          }
        }
      }
    },
    "/proxies/{id}/auto-renew": {
      "post": {
        "tags": [
          "Proxies"
        ],
        "summary": "Toggle automatic renewal",
        "description": "Renews one billing period automatically from account credit before expiry. Extended to mobile proxies: also calls upstream to enable/disable auto-renew on the line.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "enabled"
                ],
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "The new autoRenew state"
          }
        }
      }
    },
    "/proxies/{id}/rotate": {
      "post": {
        "tags": [
          "Proxies"
        ],
        "summary": "Rotate a mobile proxy IP",
        "description": "Request an immediate IP rotation for a mobile proxy line (mobile-4g, mobile-vpn, or mobile-multisim only). Not available for static ISP/GB proxies.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "{ rotated: true } — rotation initiated"
          },
          "400": {
            "description": "Invalid request (e.g., service is not a mobile proxy)"
          },
          "404": {
            "description": "Proxy service not found or not owned"
          },
          "409": {
            "description": "Service not provisioned yet"
          }
        }
      }
    },
    "/proxies/{id}/cancel": {
      "post": {
        "tags": [
          "Proxies"
        ],
        "summary": "Cancel a proxy",
        "description": "Prepaid cancel: stop-renew + run to the paid expiry. Disables auto-renew and flags the service so it will NOT renew; it stays usable until `expiresAt`, then the normal expiry path ends it. No refund (the current term is already paid). Send `{ \"cancel\": false }` to undo (restores auto-renew). Works for any proxy kind; owner-scoped.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "cancel": {
                    "type": "boolean",
                    "description": "true (default) cancels at period end; false undoes a prior cancel."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "{ cancelled, autoRenew, activeUntil } — activeUntil is the paid-through expiry."
          }
        }
      }
    },
    "/proxies/{id}/format": {
      "post": {
        "tags": [
          "Proxies"
        ],
        "summary": "Switch the proxy format",
        "description": "Switch the proxy format between HTTP/HTTPS (`http`) and SOCKS5 (`socks`). Static Residential (ISP) services only: rotating residential GB plans are provisioned as HTTP/HTTPS and their protocol cannot be changed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "protocol"
                ],
                "properties": {
                  "protocol": {
                    "type": "string",
                    "enum": [
                      "http",
                      "socks"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "The new protocol"
          }
        }
      }
    },
    "/proxies/{id}/check": {
      "post": {
        "tags": [
          "Proxies"
        ],
        "summary": "Live proxy check (exit IP, ISP, geo, health)",
        "description": "Route a small request through each of the service's proxies to report the live exit IP, ISP, city, country, up/down status and latency. Rotating (GB/mobile) proxies are checked by routing through them (a negligible amount of bandwidth); static ISP proxies exit from their own IP.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Per-proxy check results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "ip": { "type": "string" },
                          "port": { "type": "integer" },
                          "up": { "type": "boolean" },
                          "exitIp": { "type": "string", "nullable": true },
                          "isp": { "type": "string", "nullable": true },
                          "city": { "type": "string", "nullable": true },
                          "country": { "type": "string", "nullable": true },
                          "countryCode": { "type": "string", "nullable": true },
                          "latencyMs": { "type": "integer", "nullable": true },
                          "routed": { "type": "boolean" }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/proxies/{id}/replacements": {
      "get": {
        "tags": [
          "Proxies"
        ],
        "summary": "Replacement allowance and history",
        "description": "Included monthly IP-replacement allowance (1/month), usage and request history. Reflects upstream availability and replacement history.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Monthly allowance and usage"
          }
        }
      },
      "post": {
        "tags": [
          "Proxies"
        ],
        "summary": "Request the included IP replacement",
        "description": "Requests a replacement directly; the new IP appears in the proxy list once processed. Optionally pass `locationId` (a residential location id from GET /proxies/catalog) to move the new IP to a different location; omit it to replace in the same location.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "locationId": {
                    "type": "string",
                    "description": "Optional residential location id (from GET /proxies/catalog) for the new IP. Omit to replace in the same location."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "Replacement request submitted",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "description": "Allowance used. Also IDEMPOTENCY_IN_PROGRESS: a request with the same Idempotency-Key is still running; retry after Retry-After seconds with the same key."
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        }
      }
    },
    "/proxies/{id}/replacements/locations": {
      "get": {
        "tags": [
          "Proxies"
        ],
        "summary": "Locations offered for a replacement",
        "description": "Locations the new IP can be placed in for an ISP replacement (upstream-backed; distinct from the order catalog). Feed the returned `id` back as `locationId` on POST /proxies/{id}/replacements to move the new IP.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Available replacement locations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "locations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "string" },
                          "label": { "type": "string" }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/services/{id}/renew": {
      "post": {
        "tags": [
          "Services"
        ],
        "summary": "POST /services/{id}/renew",
        "description": "Ensure the service's renewal invoice and return it for payment: an existing Unpaid renewal invoice is returned as-is (the WHMCS cron generates one 15 days before the due date); otherwise one is created inside the registrar-configured early-renew window. Pay it from balance via POST /billing/invoices/{id}/pay or by card via the checkout URL. ACE-managed proxies renew via POST /proxies/{id}/renew instead.",
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "invoiceId": {
                          "type": "string"
                        },
                        "created": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            },
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/autorenew": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/autorenew",
        "description": "Whether this service auto-renews from balance. DEFAULT TRUE (policy 2026-07-07): at the due date every renewal invoice is paid automatically from promo bonus first, then real credit. An explicit false is the per-service OPT-OUT (only bonus applies automatically). ACE-managed proxies are excluded and renew via POST /proxies/{id}/renew.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      },
      "put": {
        "tags": [
          "Services"
        ],
        "summary": "PUT /services/{id}/autorenew",
        "description": "Opt a service out of (enabled:false) or back into (enabled:true) automatic renewal from balance. Default behaviour without any setting is AUTOMATIC renewal at due date, bonus first then credit; the way to let a service lapse is cancelling before the due date.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "enabled"
                ],
                "properties": {
                  "enabled": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "403": {
            "$ref": "#/components/responses/ResourceProtected"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "enabled": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/services/{id}/vpanel/status": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "GET /services/{id}/vpanel/status — probe whether the owned VPS's Virtualizor panel is reachable, before the console mounts the panel iframe. Distinguishes an infra/node outage (available:false, reason:node-unavailable) from a working panel, so the console can show honest copy instead of the vendor SPA's bare 'No VPS were found'. Ownership-scoped; cookie-only.",
        "responses": {
          "200": {
            "description": "Panel reachability for the owned VPS.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "vpsId": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "description": "The resolved Virtualizor VPS id, or null when the service is not a Virtualizor VPS."
                        },
                        "available": {
                          "type": "boolean",
                          "description": "True when the panel is reachable and lists the VPS; false during a node/license outage or when the service is not a VPS."
                        },
                        "reason": {
                          "type": "string",
                          "enum": [
                            "node-unavailable",
                            "not-a-vps"
                          ],
                          "description": "Why available is false: node-unavailable (temporary infra outage) or not-a-vps (service has no Virtualizor VPS)."
                        }
                      },
                      "required": [
                        "available"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "bearerAuth": []
          },
          {
            "cookieAuth": []
          }
        ]
      }
    },
    "/account/affiliate/withdraw": {
      "post": {
        "tags": [
          "Account"
        ],
        "summary": "Request an affiliate withdrawal (payout)",
        "description": "Requires scope `account:write`. Owner-scoped; mirrors WHMCS's native \"request withdrawal\" button. When your affiliate balance is at or above the payout minimum it opens a support ticket for an admin to process the payout in WHMCS (no funds move here). Returns `{requested:true, ticketId, balanceCents, minimumCents, currency}` on success, or `{requested:false, reason:\"not_affiliate\"|\"below_minimum\", balanceCents, minimumCents, currency}`. Blocked while impersonating.",
        "responses": {
          "200": {
            "description": "Withdrawal request outcome (ticket opened, below minimum, or not an affiliate).",
            "headers": {
              "Idempotent-Replayed": {
                "$ref": "#/components/headers/IdempotentReplayed"
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyInProgress"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ]
      }
    },
    "/account/affiliate/link": {
      "post": {
        "tags": [
          "Account"
        ],
        "summary": "Generate a signed custom affiliate redirect link",
        "description": "Requires scope `account:write`. Owner-scoped. Takes `{destination}` (an absolute http(s) URL) and returns `{link}` — a link that places the affiliate cookie (via aff_redirect.php) then redirects to the destination, reproducing the WS-Affiliates-Plus custom-link feature. The link is HMAC-signed so it cannot be forged into an open redirect. 400 `INVALID_PARAM` when the destination is not an absolute http(s) URL or the account is not yet an affiliate.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "destination"
                ],
                "properties": {
                  "destination": {
                    "type": "string",
                    "description": "Absolute http(s) URL to redirect to after the affiliate cookie is placed."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The signed custom redirect link: `{link}`."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    }
  }
}
